在Android Oreo之前,要想在一个release版本的系统中录制蓝牙HCI log,可以通过打开“开发者选项->开启蓝牙HCI信息收集日志”选项。默认蓝牙的HCI log生成文件名为“bsnoop_hci.log”,位于“sdcard”目录。但是从Android Oreo开始,google做了一些调整,原有的路子已经走不通了。如何解决呢?答案是,自己实现一个录制蓝牙HCI log的应用,从127.0.0.1:8872那里接受数据,保存到sdcard。
从UI上看,“开启蓝牙HCI信息收集日志”的开发者选项,是一个SwitchPreference。对于Oreo,具体的代码位于:
packages/apps/Settings/src/com/android/settings/development/DevelopmentSettings.java
DevelopmentSettings本身实现了很多interface,包括OnPreferenceChangeListener,因此HCI日志的开关也是在这里处理。
/*
* Displays preferences for application developers.
*/
public class DevelopmentSettings extends RestrictedSettingsFragment
implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener,
OnPreferenceChangeListener, SwitchBar.OnSwitchChangeListener, Indexable {
private static final String TAG = "DevelopmentSettings";
具体到“开启蓝牙HCI信息收集日志”选项,它的实现是设置一个property:
private void writeBtHciSnoopLogOptions() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
SystemProperties.set(BLUETOOTH_BTSNOOP_ENABLE_PROPERTY,
Boolean.toString(mBtHciSnoopLog.isChecked()));
}
仔细看上面的代码,adapter这个变量似乎是多余的。关于这一点,对比Android N的代码就可以看出来:
private void writeBtHciSnoopLogOptions() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
adapter.configHciSnoopLog(mBtHciSnoopLog.isChecked());
Settings.Secure.putInt(getActivity().getContentResolver(),
Settings.Secure.BLUETOOTH_HCI_LOG,
mBtHciSnoopLog.isChecked() ? 1 : 0);
}
跟着代码一步步往下追就会发现,Nougat在打开“开启蓝牙HCI信息收集日志”选线之后,代码调用经过jni再到native,native部分就会立即开始记录蓝牙HCI日志。在Nougat以及更早的版本中(除使用bluez作为BT stack的版本),蓝牙HCI日志默认位于sdcard目录中(保存在系统的system/etc/bluetooth/bt_stack.conf文件中),因此只要能通过adb进入系统,所有人都可以获取。
到了Oreo,打开蓝牙HCI日志的方式变成了property。首先这是一个系统property,普通用户无法修改。其次,HCI日志的保存路径也变成了一个系统property,它的默认值保存在如下路径的文件中:
system/bt/hci/src/btsnoop.cc
相关定义截取如下:
#define BTSNOOP_ENABLE_PROPERTY "persist.bluetooth.btsnoopenable"
#define BTSNOOP_PATH_PROPERTY "persist.bluetooth.btsnooppath"
#define DEFAULT_BTSNOOP_PATH "/data/misc/bluetooth/logs/btsnoop_hci.log"
#define BTSNOOP_MAX_PACKETS_PROPERTY "persist.bluetooth.btsnoopsize"
看到/data/这个路径,就应该知道老路子是走不通了。
老路子走不通,google还提供了另外一种方案——socket方式接收蓝牙HCI日志。Server部分的代码如下:
static void* listen_fn_(UNUSED_ATTR void* context) {
int enable = 1;
prctl(PR_SET_NAME, (unsigned long)LISTEN_THREAD_NAME_, 0, 0, 0);
listen_socket_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//......
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(LOCALHOST_);
addr.sin_port = htons(LISTEN_PORT_);
if (bind(listen_socket_, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
LOG_ERROR(LOG_TAG, "%s unable to bind listen socket: %s", __func__,
strerror(errno));
goto cleanup;
}
if (listen(listen_socket_, 10) == -1) {
LOG_ERROR(LOG_TAG, "%s unable to listen: %s", __func__, strerror(errno));
goto cleanup;
}
for (;;) {
int client_socket;
OSI_NO_INTR(client_socket = accept(listen_socket_, NULL, NULL));
if (client_socket == -1) {
if (errno == EINVAL || errno == EBADF) {
break;
}
LOG_WARN(LOG_TAG, "%s error accepting socket: %s", __func__,
strerror(errno));
continue;
}
/* When a new client connects, we have to send the btsnoop file header. This
* allows a decoder to treat the session as a new, valid btsnoop file. */
std::lock_guard lock(client_socket_mutex_);
safe_close_(&client_socket_);
client_socket_ = client_socket;
OSI_NO_INTR(send(client_socket_, "btsnoop\0\0\0\0\1\0\0\x3\xea", 16, 0));
}
cleanup:
safe_close_(&listen_socket_);
return NULL;
}
Server端的实现很简单,它在127.0.0.1:8872上监听前来连接的client。首次连接上,server会将16字节的header发送给client,这是HCI日志格式的一部分。后续一旦有数据在HCI层流动,server会将数据copy一份送给client。
通过socket获取蓝牙HCI日志的功能,Android从5.0就开始支持了。只是老路子还能用,谁还费神去想别的方法呢。
Socket方式确认是可以走通了,可是还有一个疑问。Android的sepolicy越来越严格,难道google没有特意去限制连接的client么?