How to programmatically force bluetooth low energy service discovery on Android without using cache
Asked Answered
L

5

46

I am using Android 4.4.2 on a Nexus 7. I have a bluetooth low energy peripheral whose services change when it is rebooted. The android app calls BluetoothGatt.discoverServices(). However Android only queries the peripheral once to discover services, subsequent calls to discoverServices() result in the cached data from the first call, even between disconnections. If I disable/enable the Android bt adapter then discoverServices() refreshes the cache by querying the peripheral. Is there a programmatic way to force Android to refresh its' ble services cache without disabling/enabling the adapter?

Lauralauraceous answered 23/3, 2014 at 20:55 Comment(2)
In mentioned answer by @Miguel localMethod.invoke(l..) sometime returning false..what that mean...any suggestion ?Prophase
Your question helped me to figure out another problem... thanks.Francklyn
A
83

I just had the same problem. If you see the source code of BluetoothGatt.java you can see that there is a method called 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;
}

This method does actually clear the cache from a bluetooth device. But the problem is that we don't have access to it. But in java we have reflection, so we can access this method. Here is my code to connect a bluetooth device refreshing the cache.

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;
    }
Aurelia answered 28/3, 2014 at 9:59 Comment(10)
The above technique allows a correct implementation of the SERVICE CHANGED indication. developer.bluetooth.org/gatt/characteristics/Pages/… There is a decent explanation of this in the core ble spec. Basically the central subscribes for indications from the peripheral if the services have changed.Lauralauraceous
But in my case, the method refreshDeviceCache() may return false sometimes!!! How to fix?Territorialism
I found why sometimes refreshDeviceCache() may return false. Because mClientIf == 0, and after onClientRegistered() called, mClientIf was assignment. And onClientRegistered() may call before or after refresh() mehtod. So, you can execute refresh() before close() connection.Territorialism
unfortunately this does not work for Galaxy S5. Their bluetooth is so customized, that i hate them.Aileenailene
Very helpful answer, @Miguel. Could you please explain why your solution declares the local variable localBluetoothGatt. Why not just use gatt that is passed in?Fugue
Nice idea, also doesn't seem to work on the Galaxy S4, though. Looks like Samsung phones tend to do their own thing.Anneliese
Seems to me it this function will force discoverServices() right after attempting to connect? It still hasn't changed state to connect, and will force to get the services. Am I right? So it will cause some kinda of problem because of that. Please someone correct me if I'm wrong.Battled
I am getting the following exception: java.lang.NullPointerException: null receiver at java.lang.reflect.Method.invoke(Native Method). Can someone please help me out?Topheavy
Reflection is not allowed on Android 9: developer.android.com/about/versions/pie/…Surprisal
If you want to see how deep is the rabbit hole, this is what refreshing device cache is fluoride.docsforge.com/master/api-bta/…. Each BT chip has it's own db, basically any GATT info is deleted from this db.Vestryman
E
3

Here is the Kotlin version with RxAndroidBle for refresh:

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
  }
}

Actual call:

Observable.just(rxBleConnection)
    .flatMap { rxBleConnection -> rxBleConnection.queue(CustomRefresh()) }
    .observeOn(Schedulers.io())
    .doOnComplete{
        switchToDFUmode()
    }
    .subscribe({ isSuccess ->
      // check 
    },
    { throwable ->
        Timber.d(throwable)
    }).also {
        refreshDisposable.add(it)
    }
Elidaelidad answered 9/1, 2019 at 14:35 Comment(1)
Invoking the private refresh() of the BluetoothGatt helps me retrieve our custom GATT service of a paired BLE connection. My issue: * Mobile device pair/bond successfully with a BluetoothGattServer * The BluetoothGattServer restarts, Then the mobile device can not retrieve the service any more (although the connection and bonding status are correct).Whorled
M
2

Indeed Miguel's answer works. To use refreshDeviceCache, I'm successful with this calling order:

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

This works for OS 4.3 through 5.0 tested with Android and iPhone Peripherals.

Martyrize answered 29/1, 2015 at 15:28 Comment(3)
sometimes refreshDeviceCache(gatt) return false as well.. what that mean ?Prophase
Two ways refreshDeviceCache returns false is from a local (or remote cache exception (through the reflection call to BluetoothGatt.refresh()). The refresh notes are "Clears the internal cache and forces a refresh of the services from the remote device." There is no detailed explanation whether the cache is cleared on failure or even if the remote service is refreshed. In this case, I would stop and fully restart the BLE session.Martyrize
Restart BLE session means bluetoothgatt.disconnect(), then reconnect with somedevice.connect(). This will remove the BLE connection with somedevice, and then establish the connection again. It resets all the communication from peripheral to central device.Martyrize
B
2

In Some Devices, Even you disconnect the socket the connection wont end because of the cache. You need to disconnect the remote device by using the BluetoothGatt Class. As Below

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

Note : This logic worked for me in china based devices

Baber answered 31/12, 2015 at 9:36 Comment(1)
Mis-typed gatt is mBluetoothGattBaber
F
1

Use the following before scanning the device:

if(mConnectedGatt != null) mConnectedGatt.close();

This will disconnect the device and clear the cache and hence you would be able to reconnect to the same device.

Frontiersman answered 2/2, 2015 at 18:25 Comment(2)
Why would you assume this is correct? Calling mConnectedGatt.close() does not clear the GATT cache.Byyourleave
How it's gonna give changed services if we do "mConnectedGatt.close()" and reconnect it ?Oldtimer

© 2022 - 2024 — McMap. All rights reserved.