package com.idevicesinc.sweetblue; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.text.TextUtils; import com.idevicesinc.sweetblue.BleManager.UhOhListener.UhOh; import com.idevicesinc.sweetblue.utils.Utils_String; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import static com.idevicesinc.sweetblue.BleManagerState.ON; final class P_NativeDeviceWrapper { private final BleDevice m_device; private P_NativeDeviceLayer m_device_native; private final String m_address; private String m_name_native; private String m_name_normalized; private String m_name_override; private int m_bondState_cached = BluetoothDevice.BOND_NONE; //--- DRK > We have to track connection state ourselves because using //--- BluetoothManager.getConnectionState() is slightly out of date //--- in some cases. Tracking ourselves from callbacks seems accurate. private AtomicInteger m_nativeConnectionState = null; public P_NativeDeviceWrapper(BleDevice device, P_NativeDeviceLayer nativeLayer, String name_normalized, String name_native) { m_device = device; m_device_native = nativeLayer; m_address = m_device_native.getAddress() == null || m_device.isNull() ? BleDevice.NULL_MAC() : m_device_native.getAddress(); m_nativeConnectionState = new AtomicInteger(-1); updateName(name_native, name_normalized); //--- DRK > Manager can be null for BleDevice.NULL. final boolean hitDiskForOverrideName = true; final String name_disk = getManager() != null ? getManager().m_diskOptionsMngr.loadName(m_address, hitDiskForOverrideName) : null; if( name_disk != null ) { setName_override(name_disk); } else { setName_override(m_name_native); final boolean saveToDisk = BleDeviceConfig.bool(m_device.conf_device().saveNameChangesToDisk, m_device.conf_mngr().saveNameChangesToDisk); getManager().m_diskOptionsMngr.saveName(m_address, m_name_native, saveToDisk); } } private BleManager getManager() { return m_device.getManager(); } private P_Logger getLogger() { return m_device.getManager().getLogger(); } void updateNativeDeviceOnly(final P_NativeDeviceLayer device_native) { m_device_native.setNativeDevice(device_native.getNativeDevice()); } void updateNativeDevice(final P_NativeDeviceLayer device_native, final byte[] scanRecord_nullable, boolean isSameScanRecord) { if (!isSameScanRecord) { String name_native; try { name_native = getManager().getDeviceName(device_native, scanRecord_nullable); } catch (Exception e) { getLogger().e("Failed to parse name, returning what BluetoothDevice returns."); name_native = device_native.getName(); } if (!TextUtils.equals(name_native, m_name_native)) { updateNativeName(name_native); } } m_device_native.setNativeDevice(device_native.getNativeDevice()); } void setName_override(final String name) { m_name_override = name != null ? name : ""; } void updateNativeName(final String name_native) { final String name_native_override; if( name_native != null ) { name_native_override = name_native; } else { //--- DRK > After a ble reset using cached devices, calling updateNativeDevice with the old native BluetoothDevice //--- instance gives you a null name...not sure how long this has been the case, but I think only > 5.0 name_native_override = m_name_native; } final String name_normalized = Utils_String.normalizeDeviceName(name_native_override); updateName(name_native_override, name_normalized); } void clearName_override() { setName_override(m_name_native); } private void updateName(String name_native, String name_normalized) { name_native = name_native != null ? name_native : ""; m_name_native = name_native; m_name_normalized = name_normalized; } public String getAddress() { if( m_device_native != null ) { m_device.getManager().ASSERT(m_address.equals(m_device_native.getAddress())); } return m_address; } public String getNormalizedName() { return m_name_normalized; } public String getNativeName() { return m_name_native; } public String getName_override() { return m_name_override; } public String getDebugName() { String[] address_split = m_address.split(":"); String lastFourOfMac = address_split[address_split.length - 2] + address_split[address_split.length - 1]; String debugName = m_name_normalized.length() == 0 ? "<no_name>" : m_name_normalized; String debug = m_device_native != null ? String.format("%s%s%s", debugName, "_", lastFourOfMac) : debugName; return debug; } public P_NativeDeviceLayer getDeviceLayer() { return m_device_native; } public BluetoothDevice getDevice() { if( m_device.isNull() ) { return m_device.getManager().newNativeDevice(BleDevice.NULL_MAC()).getNativeDevice(); } else { return m_device_native.getNativeDevice(); } } public BluetoothGatt getGatt() { return m_device.layerManager().getGattLayer().getGatt(); } private void updateGattFromCallback(BluetoothGatt gatt) { if (gatt == null && !getManager().m_config.unitTest) { getLogger().w("Gatt object from callback is null."); } else { setGatt(gatt); } } void updateGattInstance(BluetoothGatt gatt) { updateGattFromCallback(gatt); } void updateNativeConnectionState(BluetoothGatt gatt) { updateNativeConnectionState(gatt, null); } void updateNativeConnectionState(BluetoothGatt gatt, Integer state) { if( state == null ) { m_nativeConnectionState.set(getNativeConnectionState()); } else { m_nativeConnectionState.set(state); } updateGattFromCallback(gatt); getLogger().i(getLogger().gattConn(m_nativeConnectionState.get())); } public int getNativeBondState() { int bondState_native; //--- > RB If Bluetooth is not on, then we can't get the current bond state. This is here to catch the times when the system // decides to turn BT off/on. This prevents spamming the logs with a bunch of messages about not being able to get the // bond state if (m_device_native != null && getManager().is(ON)) { bondState_native = m_device_native.getBondState(); m_bondState_cached = bondState_native; } else { bondState_native = m_bondState_cached; } return bondState_native; } boolean isNativelyBonding() { return getNativeBondState() == BluetoothDevice.BOND_BONDING; } boolean isNativelyBonding(int bondState) { return bondState == BluetoothDevice.BOND_BONDING; } boolean isNativelyBonded() { return getNativeBondState() == BluetoothDevice.BOND_BONDED; } boolean isNativelyBonded(int bondState) { return bondState == BluetoothDevice.BOND_BONDED; } boolean isNativelyUnbonded() { return getNativeBondState() == BluetoothDevice.BOND_NONE; } boolean isNativelyUnbonded(int bondState) { return bondState == BluetoothDevice.BOND_NONE; } boolean isNativelyConnected() { synchronized (this) { return getConnectionState() == BluetoothGatt.STATE_CONNECTED; } } boolean isNativelyConnecting() { return getConnectionState() == BluetoothGatt.STATE_CONNECTING; } boolean isNativelyDisconnecting() { return getConnectionState() == BluetoothGatt.STATE_DISCONNECTING; } boolean isNativelyConnectingOrConnected() { int state = getConnectionState(); return state == BluetoothGatt.STATE_CONNECTED || state == BluetoothGatt.STATE_CONNECTING; } public int getNativeConnectionState() { //--- > RB If BT has been turned off quickly (for instance, there are many devices connected, then you go into BT settings and run a scan), then // we obviously won't be able to get a state. We return DISCONNECTED here for obvious reasons. if (getManager().m_config.nativeManagerLayer.isBluetoothEnabled()) { return m_device.layerManager().getManagerLayer().getConnectionState(m_device_native, BluetoothGatt.GATT_SERVER); } else { return BluetoothGatt.STATE_DISCONNECTED; } } public int getConnectionState() { return performGetNativeState(null, null); // It was thought that getting the state from the UI thread would alleviate some issues. It wasn't found to be the case // I'm leaving it here just in case we need to switch back. // if (Utils.isOnMainThread()) // { // return performGetNativeState(null, null); // } // else // { // // > RB - This may not be necessary anymore. It was thought that the check of the native state had to be made on the UI thread, // // so this is here to block the current thread until it gets the result back. // final CountDownLatch latch = new CountDownLatch(1); // final AtomicInteger state = new AtomicInteger(-1); // getManager().getPostManager().postToMain(new Runnable() // { // @Override public void run() // { // performGetNativeState(state, latch); // } // }); // try // { // latch.await(); // } catch (Exception e) // { // e.printStackTrace(); // } // return state.get(); // } } private int performGetNativeState(AtomicInteger state, CountDownLatch latch) { final int reportedNativeConnectionState = getNativeConnectionState(); int connectedStateThatWeWillGoWith = reportedNativeConnectionState; if( m_nativeConnectionState != null && m_nativeConnectionState.get() != -1 ) { if( m_nativeConnectionState.get() != reportedNativeConnectionState ) { getLogger().e("Tracked native state " + getLogger().gattConn(m_nativeConnectionState.get()) + " doesn't match reported state " + getLogger().gattConn(reportedNativeConnectionState) + "."); } connectedStateThatWeWillGoWith = m_nativeConnectionState.get(); } if( connectedStateThatWeWillGoWith != BluetoothGatt.STATE_DISCONNECTED ) { if( gattLayer().isGattNull() ) { //--- DRK > Can't assert here because gatt can legitmately be null even though we have a connecting/ed native state. //--- This was observed on the moto G right after app start up...getNativeConnectionState() reported connecting/ed //--- but we haven't called connect yet. Really rare...only seen once after 4 months. if( m_nativeConnectionState == null ) { getLogger().e("Gatt is null with " + getLogger().gattConn(connectedStateThatWeWillGoWith)); connectedStateThatWeWillGoWith = BluetoothGatt.STATE_DISCONNECTED; getManager().uhOh(UhOh.CONNECTED_WITHOUT_EVER_CONNECTING); } else { getManager().ASSERT(false, "Gatt is null with tracked native state: " + getLogger().gattConn(connectedStateThatWeWillGoWith)); } } } else { //--- DRK > Had this assert here but must have been a brain fart because we can be disconnected //--- but still have gatt be not null cause we're trying to reconnect. // if( !m_mngr.ASSERT(m_gatt == null) ) // { // m_logger.e(m_logger.gattConn(connectedStateThatWeWillGoWith)); // } } if (state != null) { state.set(connectedStateThatWeWillGoWith); } if (latch != null) { latch.countDown(); } return connectedStateThatWeWillGoWith; } void closeGattIfNeeded(boolean disconnectAlso) { m_device.m_reliableWriteMngr.onDisconnect(); if( gatt() == null ) return; closeGatt(disconnectAlso); } private void closeGatt(boolean disconnectAlso) { UhOh uhoh = m_device.layerManager().getGattLayer().closeGatt(); if (uhoh != null) { m_device.getManager().uhOh(uhoh); } m_nativeConnectionState.set(BluetoothGatt.STATE_DISCONNECTED); } private P_GattLayer gattLayer() { return m_device.layerManager().getGattLayer(); } private BluetoothGatt gatt() { return gattLayer().getGatt(); } private void setGatt(BluetoothGatt gatt) { if( gatt() != null ) { //--- DRK > This tripped with an S5 and iGrillv2 with low battery (not sure that matters). //--- AV was able to replicate twice but was not attached to debugger and now can't replicate. //--- As a result of a brief audit, moved gatt object setting from the ending state //--- handler of the connect task in P_BleDevice_Listeners to the execute method of the connect task itself. //--- Doesn't solve any particular issue found, but seems more logical. getManager().ASSERT(gatt() == gatt, "Different gatt object set."); if( gatt() != gatt ) { closeGatt(/*disconnectAlso=*/false); } else { return; } } if( gatt == null ) { gattLayer().setGatt(null); } else { gattLayer().setGatt(gatt); } } }