如何在Android上以编程方式强制执行蓝牙低功耗服务发现,而不使用缓存。

44

我正在使用Nexus 7上的Android 4.4.2系统。我有一个低功耗蓝牙外设,当它重新启动时其服务将发生变化。 安卓应用程序调用BluetoothGatt.discoverServices()。 然而,安卓只会查询一次外设来发现服务,再次调用discoverServices()会导致缓存数据是第一次调用的结果,即使在断开连接之间也是如此。 如果我禁用/启用Android的蓝牙适配器,然后discoverServices()将通过查询外设刷新缓存。 有没有编程方式可以强制安卓在不禁用/启用适配器的情况下刷新其ble服务缓存?


在@Miguel提供的答案中,localMethod.invoke(l..)有时会返回false...这是什么意思...有什么建议吗? - CoDe
你的问题帮助我解决了另一个问题...谢谢。 - benchuk
5个回答

83

我刚遇到了同样的问题。如果您查看BluetoothGatt.java的源代码,您会发现有一个名为refresh()的方法。

/**
* Clears the internal cache and forces a refresh of the services from the 
* remote device.
* @hide
*/
public boolean refresh() {
        if (DBG) Log.d(TAG, "refresh() - device: " + mDevice.getAddress());
        if (mService == null || mClientIf == 0) return false;

        try {
            mService.refreshDevice(mClientIf, mDevice.getAddress());
        } catch (RemoteException e) {
            Log.e(TAG,"",e);
            return false;
        }

        return true;
}

这种方法实际上可以清除蓝牙设备的缓存。但问题在于我们无法访问它。 但是在Java中,我们有反射,因此我们可以访问该方法。以下是我的代码,用于连接并刷新蓝牙设备的缓存。

private boolean refreshDeviceCache(BluetoothGatt gatt){
    try {
        BluetoothGatt localBluetoothGatt = gatt;
        Method localMethod = localBluetoothGatt.getClass().getMethod("refresh", new Class[0]);
        if (localMethod != null) {
           boolean bool = ((Boolean) localMethod.invoke(localBluetoothGatt, new Object[0])).booleanValue();
            return bool;
         }
    } 
    catch (Exception localException) {
        Log.e(TAG, "An exception occurred while refreshing device");
    }
    return false;
}

    
    public boolean connect(final String address) {
           if (mBluetoothAdapter == null || address == null) {
            Log.w(TAG,"BluetoothAdapter not initialized or unspecified address.");
                return false;
        }
            // Previously connected device. Try to reconnect.
            if (mBluetoothGatt != null) {
                Log.d(TAG,"Trying to use an existing mBluetoothGatt for connection.");
              if (mBluetoothGatt.connect()) {
                    return true;
               } else {
                return false;
               }
        }

        final BluetoothDevice device = mBluetoothAdapter
                .getRemoteDevice(address);
        if (device == null) {
            Log.w(TAG, "Device not found.  Unable to connect.");
            return false;
        }

        // We want to directly connect to the device, so we are setting the
        // autoConnect
        // parameter to false.
        mBluetoothGatt = device.connectGatt(MyApp.getContext(), false, mGattCallback));
        refreshDeviceCache(mBluetoothGatt);
        Log.d(TAG, "Trying to create a new connection.");
        return true;
    }

以上技术允许正确实现“服务更改”指示。在核心蓝牙规范中有一个不错的解释。基本上,如果服务发生了更改,中央会从外围设备订阅指示。 - monzie
6
很遗憾,这对于Galaxy S5无法奏效。他们的蓝牙定制化程度太高了,我很讨厌它们。 - Vladyslav Matviienko
1
非常有帮助的答案,@Miguel。您能否解释一下为什么您的解决方案声明了本地变量localBluetoothGatt?为什么不直接使用传入的gatt? - weiy
1
不错的想法,但是在Galaxy S4上似乎不起作用。看起来三星手机倾向于做自己的事情。 - jcady
2
Android 9 不允许反射:https://developer.android.com/about/versions/pie/restrictions-non-sdk-interfaces - Alix
显示剩余5条评论

2

确实,Miguel的回答是有效的。要使用refreshDeviceCache,我成功地按照以下调用顺序:

// Attempt GATT connection
public void connectGatt(MyBleDevice found) {
    BluetoothDevice device = found.getDevice();
    gatt = device.connectGatt(mActivity, false, mGattCallback);
    refreshDeviceCache(gatt);
}

此方法可用于OS 4.3至5.0,已在Android和iPhone外设上进行测试。

1
有时候 refreshDeviceCache(gatt) 也会返回 false,这是什么意思? - CoDe
refreshDeviceCache返回false的两种情况是本地(或远程缓存异常(通过反射调用BluetoothGatt.refresh())。刷新说明为“清除内部缓存并强制从远程设备刷新服务。”没有详细的解释,无论在失败时是否清除缓存,甚至是否刷新远程服务。在这种情况下,我会停止并完全重新启动BLE会话。 - Thomas
谢谢,您所说的重新启动BLE会话是什么意思! - CoDe
1
重新启动BLE会话意味着bluetoothgatt.disconnect(),然后使用somedevice.connect()重新连接。这将删除与somedevice的BLE连接,然后再次建立连接。它会重置从外围设备到中央设备的所有通信。 - Thomas

2

在某些设备上,即使您断开插座连接,由于缓存的原因,连接也不会结束。您需要使用BluetoothGatt类断开远程设备的连接。如下所示:

BluetoothGatt mBluetoothGatt = device.connectGatt(appContext, false, new BluetoothGattCallback() {
        };);
mBluetoothGatt.disconnect();

注意:这个逻辑对我在中国的设备上有效。

1
输入错误的Gatt是mBluetoothGatt。 - Satheesh

2
这是使用RxAndroidBle进行刷新的Kotlin版本:
class CustomRefresh: RxBleRadioOperationCustom<Boolean> {

  @Throws(Throwable::class)
  override fun asObservable(bluetoothGatt: BluetoothGatt,
                          rxBleGattCallback: RxBleGattCallback,
                          scheduler: Scheduler): Observable<Boolean> {

    return Observable.fromCallable<Boolean> { refreshDeviceCache(bluetoothGatt) }
        .delay(500, TimeUnit.MILLISECONDS, Schedulers.computation())
        .subscribeOn(scheduler)
  }

  private fun refreshDeviceCache(gatt: BluetoothGatt): Boolean {
    var isRefreshed = false

    try {
        val localMethod = gatt.javaClass.getMethod("refresh")
        if (localMethod != null) {
            isRefreshed = (localMethod.invoke(gatt) as Boolean)
            Timber.i("Gatt cache refresh successful: [%b]", isRefreshed)
        }
    } catch (localException: Exception) {
        Timber.e("An exception occured while refreshing device" + localException.toString())
    }

    return isRefreshed
  }
}

实际调用:

Observable.just(rxBleConnection)
    .flatMap { rxBleConnection -> rxBleConnection.queue(CustomRefresh()) }
    .observeOn(Schedulers.io())
    .doOnComplete{
        switchToDFUmode()
    }
    .subscribe({ isSuccess ->
      // check 
    },
    { throwable ->
        Timber.d(throwable)
    }).also {
        refreshDisposable.add(it)
    }

1
在扫描设备之前,请使用以下代码:
if(mConnectedGatt != null) mConnectedGatt.close();

这将断开设备连接并清除缓存,因此您可以重新连接到同一设备。

1
你为什么认为这是正确的?调用 mConnectedGatt.close() 不会清除 GATT 缓存。 - IgorGanapolsky
如果我们执行“mConnectedGatt.close()”并重新连接,它会如何提供更改后的服务? - rahul

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接