致谢
首先感谢酷安各位小米设备拥有者的NFC数据的无私分享,目前设备互联的NFC协议已经基本解析完毕。
酷安名:@星逝之殇、@云鹤霄、@潮風漢韵、@ChsBuffer、@醋友29999999、@橙米、@Cp0204。
本文初期只解析了一碰妙享的NFC数据,目前已经可以涵盖一碰妙享(屏幕镜像,文件快传),小米音箱妙播,小米蓝牙音箱的协议。
从代码来看,以下解析内容已经基本覆盖了多数可能的NFC数据类型,如有错误和更新欢迎指出。
本文重写并更新了之前的内容,修复了一些错误的描述,将协议和样例分开便于各位查看。
应用版本
所有的分析过程建立在对HyperOS中提取出的APP上,一碰妙享在小米电脑管家4.0.0.533上通过测试。
| 名称 | 包名 | 版本 |
|---|---|---|
| 小米互联通信服务 | com.xiaomi.mi_connect_service |
3.1.453.10 |
| 投屏 | com.milink.service |
15.0.5.0.ceaac61.2919843 |
| 米家 | com.xiaomi.smarthome |
9.1.501 |
| NFC服务 | com.android.nfc |
14 |
NFC工具
分析的内容已经被整合为一个安卓App,提供相关的NFC工具
项目开源地址:
NDEF解析
一条NDEF的Message可以包含多条Record数据,安卓默认以第一条为基础寻找接收NFC数据的应用。
NDEF的数据格式此处不再赘述,除了安卓自带一套方法外,也存在直接可用的库。
小米的NFC数据都是建立在NDEF格式上的,一碰妙享和小米音箱妙播的NDEF数据都只有一个Record。
Record 0
tnf: 4 [EXTERNAL]
type: b'com.xiaomi.mi_connect_service:externaltype'
payload: ... ...
或者小米碰碰贴2
Record 0
tnf: 4 [EXTERNAL]
type: b'com.xiaomi.smarthome:externaltype'
payload: ... ...
小米蓝牙音箱还有另一条Record数据,这个Record的作用是给非小米设备扫描到时主动弹出米家主页用的,所以可以忽略。
Record 1
tnf: 1 [WELL_KNOWN]
type: b'U' [URI]
payload: 04:67:2e:68:6f:6d:65:2e:6d:69:2e:63:6f:6d (https://g.home.mi.com)
小米碰碰贴2还有用于打开指定包名的应用的两条记录。
tnf: 4 [EXTERNAL]
type: b'android.com:pkg'
payload: 63:6F:6D:2E:78:69:61:6F:6D:69:2E:73:6D:61:72:74:68:6F:6D:65 (com.xiaomi.smarthome)
tnf: 4 [EXTERNAL]
type: b'android.com:pkg'
payload: 63:6F:6D:2E:78:69:61:6F:6D:69:2E:6D:69:5F:63:6F:6E:6E:65:63:74:5F:73:65:72:76:69:63:65 (com.xiaomi.mi_connect_service)
载荷数据分析
小米在原生的NFC服务(com.android.nfc)基础上,增加了自有NDEF类型可以调用系统应用的功能,其他情况下均会弹出不同类型的通知,用户点击后才会实际发送NFC标签的广播或者执行相应操作。
其中,当NDEF类型为上文提到的externaltype类型时,除了NDEF的Action之外,还会以vnd.android.nfc://ext/[type]的Uri寻找Activity启动。
根据安卓官方文档,要处理这种类型的数据,就需要APP在Manifest的activity中声明以下内容:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="vnd.android.nfc" android:host="ext" android:pathPrefix="/[type]"/>
</intent-filter>
使用ADB命令在手机上搜索可以得到com.xiaomi.mi_connect_service/.nfc.NfcFieldOnPublisher和com.xiaomi.smarthome/.nfctag.ui.NFCReadActivity:
adb shell cmd package query-activities --components -d "vnd.android.nfc://ext/com.xiaomi.mi_connect_service:externaltype"
adb shell cmd package query-activities --components -d "vnd.android.nfc://ext/com.xiaomi.smarthome:externaltype"
com.xiaomi.mi_connect_service包名的App,名称为小米互联通信服务。
查看其中的组件,发现这个载荷使用了ProtoBuf的数据存储格式,在APP的资源文件中进行对比可以找到AttrProto.proto文件。
com.xiaomi.smarthome包名的App,名称为米家,其中协议主体部分相同,不同之处会在后文分别介绍。
以下为ProtoBuf部分截取的片段:
syntax = "proto3";
package mi_connect_service;
message AttrAdvData {
int32 versionMajor = 1;
int32 versionMinor = 2;
bytes apps = 3;//已过时。请使用appIds替代。
bytes flags = 4;
string name = 5;
bytes idHash = 6;
int32 deviceType = 7;
int32 securityMode = 8;
repeated bytes appsData = 9;
repeated bytes supportSetting = 10;
repeated bytes currentSetting = 11;
string wifiMac = 12;
repeated int32 appIds = 13;//引入appIds之前使用的是apps
int32 commData = 14;
bool ziped = 15;
string wiredMac = 16;//有线网卡mac
string btMac = 17; //Bluetooth Device MAC
}
message AttrOps {
AttrAdvData advData = 1;
int32 sequenceId = 14;
}
载荷中的数据为AttrOps格式,但是由于sequenceId在样例中没有出现过并且NDEF解析时也从未使用过这个字段,所以可以直接看AttrAdvData。
AttrAdvData
所有NFC标签中AttrAdvData的deviceType均为:15
所有NFC标签中AttrAdvData的name均为: “MI-NFCTAG”
appId
| 数字 | 名称 |
|---|---|
| 1 | MI_SHARE |
| 2 | MI_PLAY |
| 62 | MI_ACCOUNT |
| 63 | MI_MOVER |
| 16382 | MI_TEST |
| 3 | MI_MIRROR |
| 16381 | MI_REMOTE_CONTROLLER |
| 16379 | MI_VOIP |
| 16377 | MI_VIDEO_RELAY |
| 16378 | MI_TAP |
| 16376 | MI_WATCH_CAMERA |
所有NFC标签中AttrAdvData的appId均为:MI_TAP (16378)
flags
| 数字 | 操作 |
|---|---|
| 0 或 Others | 发送 com.xiaomi.aiot.nfc.message 本地广播,转发NDEF的Tag和Record数据,aiot协议,V1版本协议 |
| 1 | 发送 com.xiaomi.nfc.action.TAG_DISCOVERED 广播,使用ProtoBuf中的appsData数据(NfcTagAppData类型),V2版本协议 |
| 3 | 发送 com.xiaomi.nfc.action.[ACTION] 自定义ACTION广播,使用ProtoBuf中的appsData数据,Handoff NFC协议 |
flags=0x00 V1版本协议
目前样例中的AttrAdvData版本为:
- versionMajor: 1
- versionMinor: 2
这个版本AttrAdvData的idHash=0x00。
发送内部广播
Intent
- Action:
com.xiaomi.aiot.nfc.message - Extra:
com.xiaomi.aiot.nfc.extra.advNdefRecord.getPayload() 格式为AttrAdvData的byte[]数据com.xiaomi.aiot.nfc.extra.tag安卓NFC的Tag对象
若AttrAdvData中的deviceType不为15,则发送Action: com.xiaomi.aiot.nfc.scan.msg的内部广播报错。
查找com.xiaomi.aiot.nfc.message,可以发现使用NfcTagAppData解析com.xiaomi.aiot.nfc.extra.adv数据。
NfcTagAppData
| 位置 | 含义 |
|---|---|
| 0 | 主版本号 |
| 1 | 次版本号 |
| 2 ~ 5 | 写入时间,32位字节的整数,Unix时间戳 |
| 6 | flag(目前没用到) |
| 7 | Record数量,至少存在一个否则就是空标签 |
目前此处版本号无特殊定义和限制。
Record目前有两种类型NfcTagDeviceRecord和NfcTagActionRecord。
NfcTagDeviceRecord
Record类型为:0x01
| 位置 | 含义 |
|---|---|
| 0 | Record类型 |
| 1 ~ 2 | Record长度,16位整数 Short,整个长度而不只是后续数据长度 |
| 3 ~ 4 | 设备类型,16位整数 Short |
| 5 | flag(目前没用到) |
| 6 | 设备编号,无特殊定义 |
之后的数据为设备属性键值对
设备属性键值对
键值对用以下格式反复
| 位置 | 含义 |
|---|---|
| 0 ~ 1 | 类型,16位整数 Short |
| 2 ~ 3 | 值长度,16位整数 Short,设为X |
4 ~ 4+X-1 |
值,byte[] |
DeviceAttribute 设备属性
-
当NDEF类型为
com.xiaomi.mi_connect_service:externaltype时:值范围:
[0, 19)数值 名称 数据类型 1 DEVICE_ATTR_WIFI_MAC_ADDRESS byte[]2 DEVICE_ATTR_BLUETOOTH_MAC_ADDRESS byte[]3 DEVICE_ATTR_NIC_MAC_ADDRESS byte[]4 DEVICE_ATTR_IP_ADDRESS 5 DEVICE_ATTR_PORT_1 6 DEVICE_ATTR_PORT_2 7 DEVICE_ATTR_PORT_3 8 DEVICE_ATTR_ID_HASH byte[]9 DEVICE_ATTR_DEVICE_TOKEN 10 DEVICE_ATTR_AUTH_TOKEN 11 DEVICE_ATTR_DEVICE_NAME 12 DEVICE_ATTR_DEVICE_TYPE 13 DEVICE_ATTR_APP_DATA byte[]14 DEVICE_ATTR_USER_ENV_TOKEN 15 DEVICE_ATTR_SSID String17 DEVICE_ATTR_PASSWORD String18 DEVICE_ATTR_MODEL String16 或 默认 “UnSupport Attribute” 注:当DeviceAttribute为DEVICE_ATTR_APP_DATA (13)时,其数据如果以
"mxD".getBytes()开头,其中的数据与设备属性键值对的格式相同,最后将其并入所有的DeviceAttribute中。
否则可能直接存储二进制数组或者UTF-8字符串。 -
当NDEF类型为
com.xiaomi.smarthome:externaltype时:当
NfcTagActionRecord的Action=ACTION_IOT数值 名称 数据类型 1 DEVICE_ID String2 USER_MODEL String3 NFC_EXTRA_DATA String4 DEVICE_MAC String13 APP_DATA String当
NfcTagActionRecord的Action=ACTION_IOT_ENV数值 名称 数据类型 1 USER_ID String2 OWNER_UID String3 REGION String4 SCENE_NAME String其余情况参照NDEF类型为
com.xiaomi.mi_connect_service:externaltype时的情况
DeviceType 设备类型
值范围:[0, 8)
| 数值 | 名称 |
|---|---|
| 1 | DEVICE_TYPE_IOT |
| 2 | DEVICE_TYPE_MI_ROUTER |
| 3 | DEVICE_TYPE_MI_SOUND_BOX |
| 4 | DEVICE_TYPE_MI_LAPTOP |
| 5 | DEVICE_TYPE_MI_TV |
| 6 | DEVICE_TYPE_MI_PHONE |
| 7 | DEVICE_TYPE_IOT_USER_ENV |
| 默认 | “Unsupported device” |
NfcTagActionRecord
Record类型为:0x02
| 位置 | 含义 |
|---|---|
| 0 | Record类型 |
| 1 ~ 2 | Record长度,16位整数 Short,整个长度而不只是后续数据长度 |
| 3 ~ 4 | action,16位整数 Short |
| 5 | condition,byte |
| 6 | 设备编号,无特殊定义 |
| 7 | flag(目前没用到) |
| 之后全部 | conditionParameters(目前没用到) |
Action
值范围:[0, 14)
| 数值 | 名称 |
|---|---|
| 1 | ACTION_IOT |
| 2 | ACTION_MUSIC_RELAY |
| 3 | ACTION_TEL_RELAY |
| 4 | ACTION_FILE_TRANSFER |
| 5 | ACTION_SCREEN_CASTING |
| 6 | ACTION_CORP_OPERATION |
| 7 | ACTION_VIDEO_RELAY |
| 8 | ACTION_VOIP_RELAY |
| 9 | ACTION_IOT_ENV |
| 10 | ACTION_REMOTE_CONTROLLER |
| 11 | ACTION_GUEST_NETWORK |
| 12 | ACTION_EMPTY |
| 13 | ACTION_CUSTOM |
| 32767 | ACTION_AUTO (Short.MAX_VALUE) |
| 默认 | “Unsupported” |
注:ACTION_CUSTOM (13) 在代码中并无说明,只是为了方便引用描述自主加上的,目前只在AttrAdvData的flags=0x01时使用。
Condition
值范围:[0, 3)
| 数值 | 名称 |
|---|---|
| 1 | CONDITION_APP_FOREGROUND |
| 2 | CONDITION_SCREEN_LOCKED |
| 127 | CONDITION_AUTO (Byte.MAX_VALUE) |
NfcExecutorFactory NFC事件分发
事件分发以Action为区分,如以下未列出则当NDEF类型为com.xiaomi.mi_connect_service:externaltype时无事件分发。
当NDEF类型为com.xiaomi.smarthome:externaltype时有另外的分发方案,此处不做讨论。
AppDiscTypeEnum
| 名称 | 优先级 | ID | 是否广泛支持 |
|---|---|---|---|
| NONE | -1 | 0 | false |
| BT | 1 | 1 | true |
| BLE | 1 | 64 | true |
| BT_CLASSIC | 1 | 128 | true |
| IP_BONJOUR | 2 | 2 | true |
| IP_P2P | 3 | 16 | true |
| IP_SOFTAP | 4 | 32 | true |
| NFC | 5 | 4 | false |
ACTION_SCREEN_CASTING
发送广播
Intent
- Action:
com.xiaomi.mi_connect_service.mi_play_endpoint_found - Permission:
com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT - Flags:
16777216安卓FlagIntent.FLAG_ACTIVITY_PREVIOUS_IS_TOP32安卓FlagIntent.FLAG_INCLUDE_STOPPED_PACKAGES268435456小米自定义FlagStaticConfigServiceConnector.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
- Extra:
macDEVICE_ATTR_WIFI_MAC_ADDRESS (Mac的16进制字符串,用:连接)disctype4 AppDiscTypeEnum.NFC的整数IDrssi整数 0name字符串资源R.string.xiaomi_tv
英文和中文下为:“Xiaomi TV” / “小米电视”idhash若action=com.milink.service.CMD,则new String(DEVICE_ATTR_ID_HASH),否则直接使用DEVICE_ATTR_ID_HASHcmd整数 1wired_macDEVICE_ATTR_NIC_MAC_ADDRESS (Mac的16进制字符串,用:连接)bt_macDEVICE_ATTR_BLUETOOTH_MAC_ADDRESS (Mac的16进制字符串,用:连接)
ACTION_VOIP_RELAY
发送广播
Intent
- Action:
com.xiaomi.mi_connect_service.nfc_voip_relay_endpoint_found - Permission:
com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT - Flags:
16777216安卓FlagIntent.FLAG_ACTIVITY_PREVIOUS_IS_TOP32安卓FlagIntent.FLAG_INCLUDE_STOPPED_PACKAGES268435456小米自定义FlagStaticConfigServiceConnector.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
- Extra:
macDEVICE_ATTR_WIFI_MAC_ADDRESS (Mac的16进制字符串,用:连接)idhashDEVICE_ATTR_ID_HASHbyte[]disctype4 AppDiscTypeEnum.NFC的整数IDname字符串资源R.string.xiaomi_nfc_tag_device
英文和中文下为:“Mi Device” / “小米碰碰贴的设备”
ACTION_AUTO
若DEVICE_ATTR_MODEL为空,则默认为"screen"。
若设备类型为DEVICE_TYPE_MI_TV (5),则执行ACTION_SCREEN_CASTING相同的分发。
若设备类型为DEVICE_TYPE_MI_SOUND_BOX (3),则:
- 若前台App为
com.xiaomi.mitime(小米通话),则执行ACTION_SCREEN_CASTING相同的分发。 - 否则执行
ACTION_MUSIC_RELAY或ACTION_TEL_RELAY相同的分发。
其它设备类型,则执行ACTION_MUSIC_RELAY或ACTION_TEL_RELAY相同的分发。
ACTION_GUEST_NETWORK
需要 DEVICE_ATTR_SSID (byte[] 转 UTF-8 字符串)
需要 DEVICE_ATTR_PASSWORD (byte[] 转 UTF-8 字符串)
这两个数据用于连接WIFI
ACTION_EMPTY
如果未安装com.xiaomi.smarthome(米家):
Intent
- Action:
android.intent.action.VIEW - Data:
market://details?id=com.xiaomi.smarthome - Flags: BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE = 268435456
启动应用市场Activity安装com.xiaomi.smarthome(米家)
如果已安装则不处理,让com.xiaomi.smarthome(米家)去处理
ACTION_MUSIC_RELAY或ACTION_TEL_RELAY
需要 DEVICE_ATTR_MODEL (byte[] 转 UTF-8 字符串)
需要 DEVICE_ATTR_BLUETOOTH_MAC_ADDRESS
连接到蓝牙并播放媒体(A2DP)
flags=0x01 V2版本协议
目前样例中的AttrAdvData版本为:
- versionMajor: 1
- versionMinor: 11
这个版本AttrAdvData的idHash=0x00。
appsData的解析过程详见前文的NfcTagAppData,格式是一样的。
发送广播
Intent
- Action:
com.xiaomi.nfc.action.TAG_DISCOVERED - Permission:
com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINTManifest中需要声明这个权限才能收听到广播 - Flags:
16777216安卓FlagIntent.FLAG_ACTIVITY_PREVIOUS_IS_TOP32安卓FlagIntent.FLAG_INCLUDE_STOPPED_PACKAGES268435456小米自定义FlagStaticConfigServiceConnector.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
- Extra:
Action整数 ACTION_CUSTOM 13IdHash字符串Base64.encodeToString(DEVICE_ATTR_ID_HASH, Base64.DEFAULT)WifiMac字符串 DEVICE_ATTR_WIFI_MAC_ADDRESS (Mac的16进制字符串,用:连接)BtAddress字符串 DEVICE_ATTR_BLUETOOTH_MAC_ADDRESS (Mac的16进制字符串,用:连接)
注:除Action外,其他参数为空时会传递空字符串
flags=0x03 Handoff NFC协议
目前样例中的AttrAdvData版本为:
- versionMajor: 1
- versionMinor: 13
这个版本未使用AttrAdvData中的idHash,所以不用填写。
appsData
appsData中的每一个byte[]由头部,数字键值对,尾部三个部分组成。
头部
| 位置 | 含义 |
|---|---|
| 0 | 主版本号 |
| 1 | 次版本号 |
| 2 ~ 5 | 设备类型 |
主版本号和次版本号十进制为39和23,目前直接写死在代码里,只有相等时才能继续解析。
设备类型的种类详见后面的device_type_key内容。
数字键值对
| 位置 | 含义 |
|---|---|
| 6 | 数字键值对长度 |
键值对用以下格式反复
| 位置 | 含义 |
|---|---|
| 0 | 键,整数 |
| 1 | 值长度,设为X |
2 ~ 2+X-1 |
值,byte[] |
尾部
假设数字键值对读取结束后的下一位为A
| 位置 | 含义 |
|---|---|
| A | 自定义ACTION广播的名称的长度,设为X |
A+1 ~ A+1+X-1 |
自定义ACTION广播的名称 |
| 之后全部 | 发送的byte[]载荷 |
构建protocol_value_keyJSON
| 键 | 值 |
|---|---|
device_type_key |
设备类型,将32字节的设备类型转为整数 |
attribute_value_key |
数字键值对数据转JSON后,UTF-8编码为Base64的字符串 |
protocol_payload_key |
发送的byte[]载荷,转为的Base64的字符串 |
注:不存在某种数据就不存在这个JSON字段。
attribute_value_keyJSON:键数字转为字符串,目前byte[]值直接交给org.json.JSONObject#put处理(这样会出BUG,但是这个字段没被用到过)。
发送广播
目前自定义ACTION广播可用的[ACTION]只有TAG_DISCOVERED
adb shell "dumpsys package resolvers receiver | grep com.xiaomi.nfc.action."
Intent
- Action:
com.xiaomi.nfc.action.[ACTION] - Permission:
com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINTManifest中需要声明这个权限才能收听到广播 - Flags:
16777216安卓FlagIntent.FLAG_ACTIVITY_PREVIOUS_IS_TOP32安卓FlagIntent.FLAG_INCLUDE_STOPPED_PACKAGES268435456小米自定义FlagStaticConfigServiceConnector.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
- Extra:
protocol_value_key内容为JSON字符串
对于以上Intent,使用ADB命令在手机上搜索可以得到com.milink.service/com.miui.circulate.nfc.relay.NfcTransportReceiver:
adb shell cmd package query-receivers --components -a "com.xiaomi.nfc.action.TAG_DISCOVERED"
找到 com.milink.service 包名的App,名称为投屏或者互联互通服务。
接着就可以继续解析JSON字符串内的内容了。
device_type_key 设备类型定义
| 数值 | 名称 |
|---|---|
| 2 | TV |
| 3 | PC |
| 5 | CAR |
| 8 | PAD |
protocol_payload_key 载荷
键值对用以下格式反复
| 位置 | 含义 |
|---|---|
| 0 | 键,整数 |
| 1 | 值长度,设为X |
2 - 2+X-1 |
值,byte[] |
键值对
| 数值 | 名称 | 类型 | 解释 |
|---|---|---|---|
| 101 | actionSuffix | UTF-8 String | Action后缀 |
| 1 | btMac | UTF-8 String | 蓝牙Mac地址 |
| 2 | wifiMac | UTF-8 String | WIFI Mac地址 |
| 3 | wiredMac | UTF-8 String | 有线Mac地址 |
| 121 | extAbility | bytes | 额外能力 |
actionSuffix
目前已知的actionSuffix为:MIRROR和TVCAST
adb shell "dumpsys package resolvers receiver | grep com.miui.onehop.action."
extAbility
目前只有一种:_ability_lyra Boolean,要求(extAbility[0] & 1) > 0
发送自定义Action的广播
发送的是有序广播:OrderedBroadcast
Intent
- Action:
com.miui.onehop.action.[ACTION][ACTION]=actionSuffix - Permission:
com.miui.onehop.permission.MIRROR - Flags: 添加了多组flags
32安卓FlagIntent.FLAG_INCLUDE_STOPPED_PACKAGES16777216小米自定义FlagRemoteCallAdapterKt.FLAG_RECEIVER_INCLUDE_BACKGROUND
- Extra:
_device_typeint,设备类型,device_type_key_bt_macString,蓝牙Mac地址,btMac_wifi_macString,WIFI Mac地址,wifiMac_wired_macString,有线Mac地址,wiredMac_ability_lyraBoolean,(extAbility[0] & 1) > 0
这个广播目前只由Appcom.milink.service自己接收并处理。
样例解析
一碰妙享
NDEF 完整数据
d42a4d636f6d2e7869616f6d692e6d695f636f6e6e6563745f736572766963653a65787465726e616c747970650a4b0801100d2201032a094d492d4e4643544147380f4a31271700000003000e5441475f444953434f564552454465064d4952524f52011130303a30303a30303a30303a30303a30306a02fa7f
NDEF解析
Record 0
flags:
message_begin: True
message_end: True
chunked: False
short: True
id: False
tnf: 4 [EXTERNAL]
type_len: 42
type: b'com.xiaomi.mi_connect_service:externaltype'
id_len: 0
id: None
payload_len: 77
payload: 0a4b ... fa7f
payload解析
{
"advData": {
"appIds": [
16378
],
"appsData": [], // 数据过多不展示
"deviceType": 15,
"flags": "Aw==", // 0x03
"name": "MI-NFCTAG",
"versionMajor": 1,
"versionMinor": 13
}
}
数据解析
27:17:00:00:00:03:00:0e:54:41:47:5f:44:49:53:43:4f:56:45:52:45:44:65:06:4d:49:52:52:4f:52:01:11:30:30:3a:30:30:3a:30:30:3a:30:30:3a:30:30:3a:30:30
| 数据 | 名称 | 解释 |
|---|---|---|
27:17 |
版本号 | 十进制为39 23 |
00:00:00:03 |
设备类型 | PC 3 |
00 |
数字键值对数据长度 | 无数据 |
0e |
自定义ACTION长度 | 十进制为14 |
54: ... :44 |
自定义ACTION名称 | TAG_DISCOVERED |
65 |
键 | actionSuffix 101 |
06 |
值长度 | 十进制为6 |
4d: ... :52 |
值 | MIRROR |
01 |
键 | btMac 1 |
11 |
值长度 | 十进制为17 |
30: ... :30 |
值,UTF-8字符串 | 00:00:00:00:00:00 |
小米音箱妙播
NDEF 完整数据
d42a4a636f6d2e7869616f6d692e6d695f636f6e6e6563745f736572766963653a65787465726e616c747970650a480801100b2201012a094d492d4e4643544147320100380f4a2b010063034f6b000201001b000300000001000611111111111000020006111111111111020008000d7f00006a02fa7f
NDEF解析
Record 0
flags:
message_begin: True
message_end: True
chunked: False
short: True
id: False
tnf: 4 [EXTERNAL]
type_len: 42
type: b'com.xiaomi.mi_connect_service:externaltype'
id_len: 0
id: None
payload_len: 74
payload: 0a48 ... fa7f
payload解析
{
"advData": {
"appIds": [
16378
],
"appsData": [], // 数据过多不展示
"deviceType": 15,
"flags": "AQ==", // 0x01
"idHash": "AA==", // 0x00
"name": "MI-NFCTAG",
"versionMajor": 1,
"versionMinor": 11
}
}
数据解析
01:00:63:03:4f:6b:00:02:01:00:1b:00:03:00:00:00:01:00:06:11:11:11:11:11:10:00:02:00:06:11:11:11:11:11:11:02:00:08:00:0d:7f:00:00
| 数据 | 名称 | 解释 |
|---|---|---|
01:00 |
主和次版本号 | 十进制1和0 |
63:03:4f:6b |
写入时间 | 1661161323 = 2022-08-22 17:42:03 |
00 |
flag | |
02 |
Record数量 | 十进制2 |
01 |
Record类型 | NfcTagDeviceRecord |
00:1b |
Record长度 | 十进制27 |
00:03 |
设备类型 | DEVICE_TYPE_MI_SOUND_BOX 3 |
00 |
flag | |
00 |
设备编号 | 十进制0 |
00:01 |
属性类型 | DEVICE_ATTR_WIFI_MAC_ADDRESS 1 |
00:06 |
属性数据长度 | 十进制6 |
11:11:11:11:11:10 |
属性数据 | Mac地址byte[] |
00:02 |
属性类型 | DEVICE_ATTR_BLUETOOTH_MAC_ADDRESS 2 |
00:06 |
属性数据长度 | 十进制6 |
11:11:11:11:11:11 |
属性数据 | Mac地址byte[] |
02 |
Record类型 | NfcTagActionRecord |
00:08 |
Record长度 | 十进制8 |
00:0d |
Action | ACTION_CUSTOM 13 |
7f |
Condition | CONDITION_AUTO 127 |
00 |
设备编号 | 十进制0 |
00 |
flag |
小米蓝牙音箱
NDEF 完整数据
942a65636f6d2e7869616f6d692e6d695f636f6e6e6563745f736572766963653a65787465726e616c747970650a63080110022201002a094d492d4e4643544147320100380f4a460100646e0c840002010036000300000001000600000000000000020006000000000000001200177869616f6d692e77696669737065616b65722e783038630200087fff7f00006a02fa7f51010e5504672e686f6d652e6d692e636f6d
NDEF解析
Record 0
flags:
message_begin: True
message_end: False
chunked: False
short: True
id: False
tnf: 4 [EXTERNAL]
type_len: 42
type: b'com.xiaomi.mi_connect_service:externaltype'
id_len: 0
id: None
payload_len: 101
payload: 0a63 ... fa7f
Record 1
flags:
message_begin: False
message_end: True
chunked: False
short: True
id: False
tnf: 1 [WELL_KNOWN]
type_len: 1
type: b'U' [URI]
id_len: 0
id: None
payload_len: 14
payload: 04672e686f6d652e6d692e636f6d
payload解析
{
"advData": {
"appIds": [
16378
],
"appsData": [], // 数据过多不展示
"deviceType": 15,
"flags": "AA==", // 0x00
"idHash": "AA==", // 0x00
"name": "MI-NFCTAG",
"versionMajor": 1,
"versionMinor": 2
}
}
数据解析
01:00:64:6e:0c:84:00:02:01:00:36:00:03:00:00:00:01:00:06:00:00:00:00:00:00:00:02:00:06:00:00:00:00:00:00:00:12:00:17:78:69:61:6f:6d:69:2e:77:69:66:69:73:70:65:61:6b:65:72:2e:78:30:38:63:02:00:08:7f:ff:7f:00:00
| 数据 | 名称 | 解释 |
|---|---|---|
01:00 |
主和次版本号 | 十进制1和0 |
64:6e:0c:84 |
写入时间 | 1684933764 = 2023-05-24 21:09:24 |
00 |
flag | |
02 |
Record数量 | 十进制2 |
01 |
Record类型 | NfcTagDeviceRecord |
00:36 |
Record长度 | 十进制54 |
00:03 |
设备类型 | DEVICE_TYPE_MI_SOUND_BOX 3 |
00 |
flag | |
00 |
设备编号 | 十进制0 |
00:01 |
属性类型 | DEVICE_ATTR_WIFI_MAC_ADDRESS 1 |
00:06 |
属性数据长度 | 十进制6 |
00:00:00:00:00:00 |
属性数据 | Mac地址byte[] |
00:02 |
属性类型 | DEVICE_ATTR_BLUETOOTH_MAC_ADDRESS 2 |
00:06 |
属性数据长度 | 十进制6 |
00:00:00:00:00:00 |
属性数据 | Mac地址byte[] |
00:02 |
属性类型 | DEVICE_ATTR_MODEL 18 |
00:17 |
属性数据长度 | 十进制23 |
78: ... :63 |
属性数据 | xiaomi.wifispeaker.x08c |
02 |
Record类型 | NfcTagActionRecord |
00:08 |
Record长度 | 十进制8 |
7f:ff |
Action | ACTION_AUTO 32767 |
7f |
Condition | CONDITION_AUTO 127 |
00 |
设备编号 | 十进制0 |
00 |
flag |
触发小米一碰妙享的其他方法
如果想要代码触发小米一碰妙享功能有两种方法:
- 发送虚拟的NFC标签的NDEF信息
- 直接向
投屏或者互联互通服务发送广播
发送虚拟的NFC标签的NDEF信息
安卓中NDEF广播要求必须有android.nfc.extra.TAG的Tag数据,但是这个数据只有真实的NFC硬件才能生成。
因此一个安全的解决方案是,对于已有但不能修改的NFC标签,可以使用代码获取到这个NFC标签的硬件Tag数据,并对其中的内容修改后转发。
// 从Intent中获取已有的NDEF数据
val tag: Tag? = IntentCompat.getParcelableExtra(intent, NfcAdapter.EXTRA_TAG, Tag::class.java)
val id: ByteArray = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID)
val btMac = "00:00:00:00:00:00"
val hexPart1 = "0a4b0801100d2201032a094d492d4e4643544147380f4a3127170000000"
val hexPart2 = "000e5441475f444953434f564552454465064d4952524f520111"
val hexPart3 = "6a02fa7f"
// PC
val hex = hexPart1 + "3" + hexPart2 + btMac.toByteArray().toHexString() + hexPart3
val payload = hex.decodeHex()
val externalType = "com.xiaomi.mi_connect_service:externaltype"
val record = NdefRecord(NdefRecord.TNF_EXTERNAL_TYPE, externalType.toByteArray(), null, payload)
Intent(NfcAdapter.ACTION_NDEF_DISCOVERED).apply {
data = Uri.parse("vnd.android.nfc://ext/$externalType")
putExtra(NfcAdapter.EXTRA_TAG, tag)
putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, arrayOf(NdefMessage(record)))
putExtra(NfcAdapter.EXTRA_ID, id)
}.also {
ContextCompat.startActivity(this, it, null)
}
另一方面,com.xiaomi.mi_connect_service(小米互联通信服务)在处理收到的数据时,只有flags != 1且flags != 3时才会获取并处理Tag。
因此想要模拟这个广播,另一个不安全但是能用的方案就是不传递这个Tag数据,这样在当前使用场景下不会报错。
// 直接设置为空即可
val tag: Tag? = null
// 一个空的NFC Tag ID
val id: ByteArray = "000000000000".decodeHex()
// 类似的代码 ...
关于如何接收指定的NDEF数据,可以查看官方文档。
向投屏或者互联互通服务发送广播
模拟NDEF数据总可能会出现一些问题,因此另一个方案就是直接发送投屏的广播。
根据广播权限的定义,应用程序需要拥有com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT权限,才能向同样要求这个权限的广播接收器发送广播。
值得庆幸的是,这个权限定义在com.xiaomi.mi_connect_service(小米互联通信服务)中并且没有任何的签名或者系统限制,所以可以直接在测试APP的Manifest中申明这个权限。
val btMac = "00:00:00:00:00:00"
val hex = "65064d4952524f520111" + btMac.toByteArray().toHexString()
val payload = Base64.encodeToString(hex.decodeHex(), Base64.DEFAULT)
val json = JSONObject()
json.put("device_type_key", 3) // PC
json.put("protocol_payload_key", payload)
val extra = json.toString()
sendBroadcast(Intent("com.xiaomi.nfc.action.TAG_DISCOVERED").apply {
addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
addFlags(268435456)
putExtra("protocol_value_key", extra)
}, "com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT")
一碰妙享传文件
触发一碰妙享传文件的要求是文件/相册APP在前台,并且选中了要传输的文件。
如果想要触发,那么发送Intent的APP就必须在后台,或者不显示任何界面的发送完广播就退出。
Activity不显示界面可以使用以下Theme:
<activity android:theme="@android:style/Theme.NoDisplay" />
一碰妙享lyra连接
目前实测启用_ability_lyra能够提高设备发现与连接的速度,因此建议增加这个选项的数据。
在字节数组得到蓝牙地址的后面加入79:01:01的字节数组即可。