├── .codacy.yml ├── .travis.yml ├── .travis ├── before-deploy.sh ├── codesigning.asc.enc ├── deploy.sh └── settings.xml ├── LICENSE ├── README.md ├── bluetooth-manager.png ├── bm-transport-abstraction-layer.png ├── checkstyle.xml ├── pom.xml └── src ├── main └── java │ └── org │ └── sputnikdev │ └── bluetooth │ └── manager │ ├── AdapterDiscoveryListener.java │ ├── AdapterGovernor.java │ ├── AdapterListener.java │ ├── BluetoothAddressType.java │ ├── BluetoothFatalException.java │ ├── BluetoothGovernor.java │ ├── BluetoothInteractionException.java │ ├── BluetoothManager.java │ ├── BluetoothObjectType.java │ ├── BluetoothObjectVisitor.java │ ├── BluetoothSmartDeviceListener.java │ ├── CharacteristicGovernor.java │ ├── CombinedDeviceGovernor.java │ ├── CombinedGovernor.java │ ├── ConnectionStrategy.java │ ├── DeviceDiscoveryListener.java │ ├── DeviceGovernor.java │ ├── DiscoveredAdapter.java │ ├── DiscoveredDevice.java │ ├── DiscoveredObject.java │ ├── GattCharacteristic.java │ ├── GattService.java │ ├── GenericBluetoothDeviceListener.java │ ├── GovernorListener.java │ ├── GovernorState.java │ ├── ManagerListener.java │ ├── NotReadyException.java │ ├── ValueListener.java │ ├── auth │ ├── AuthenticationProvider.java │ ├── BluetoothAuthenticationException.java │ └── PinCodeAuthenticationProvider.java │ ├── impl │ ├── AbstractBluetoothObjectGovernor.java │ ├── AdapterGovernorImpl.java │ ├── BluetoothManagerBuilder.java │ ├── BluetoothManagerImpl.java │ ├── BluetoothManagerUtils.java │ ├── BluetoothObjectGovernor.java │ ├── CharacteristicGovernorImpl.java │ ├── CombinedAdapterGovernorImpl.java │ ├── CombinedCharacteristicGovernorImpl.java │ ├── CombinedDeviceGovernorImpl.java │ ├── CompletableFutureService.java │ ├── ConcurrentBitMap.java │ ├── DeferredCompletableFuture.java │ └── DeviceGovernorImpl.java │ └── transport │ ├── Adapter.java │ ├── BluetoothObject.java │ ├── BluetoothObjectFactory.java │ ├── Characteristic.java │ ├── CharacteristicAccessType.java │ ├── Descriptor.java │ ├── Device.java │ ├── Notification.java │ └── Service.java └── test ├── java └── org │ └── sputnikdev │ └── bluetooth │ └── manager │ ├── auth │ └── PinCodeAuthenticationProviderTest.java │ ├── impl │ ├── AbstractBluetoothObjectGovernorTest.java │ ├── AdapterGovernorImplTest.java │ ├── BluetoothManagerIT.java │ ├── BluetoothManagerImplTest.java │ ├── CombinedCharacteristicGovernorImplTest.java │ ├── CombinedDeviceGovernorImplTest.java │ ├── DeviceGovernorImplTest.java │ └── MockUtils.java │ ├── transport │ └── CharacteristicAccessTypeTest.java │ └── util │ ├── AdapterEmulator.java │ ├── BluetoothFactoryEmulator.java │ ├── CharacteristicEmulator.java │ └── DeviceEmulator.java └── resources └── log4j.xml /.codacy.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - '**/test/**' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | script: mvn test -P !build-extras -B -DskipITs=true 3 | notifications: 4 | slack: kolotov:69j2qLZ0paybeuQMcKfyN2gP 5 | before_deploy: git checkout master 6 | after_success: 7 | - mvn jacoco:report coveralls:report 8 | - .travis/before-deploy.sh 9 | - .travis/deploy.sh 10 | env: 11 | global: 12 | # OSSRH_JIRA_USERNAME 13 | - secure: "YkKMGox09e3FGPlypBDM2IanjBFNCkUvOjodpJcfyNIvBBzUILFpfHgMUJGJmhD3IE1zjdtfkim/hlgCem1T2vXTEj21v8xz2dK6tCGuy81zyGilJw7D7Fqs4N/J1dzLsHGyCyOspdgFG5D/waEBrK5jjj1rlbj1/XYkkTiSyx6P1FAmLI4s1jWuYESaB1uh5bD3bXEIV2s/LyZmB+Bngt1VnSEC6MguZ6EVREIaCxpNluLgFMX90JkzRaUfCTJ14mGuKMidfh3kChuKAl2XReCRAKFBpIR5vKBEVdZHa1qB5SncLKkgK2xs5q6RurVjwGBtgoYb4oyDA/K5U+Je5K+g0hXeBKQlWhFIpegHEgDu6b8jWV4KOSuTrhpKK9lpJvjKolG2I3vHMpssgoGbsbPxZbtb8nwfYhd3CDhVJd9NsC9Ni9f8rqR+61QDEzPFuHj9QKm7u9DBmC9CfcoAX3S96id3W9s9zcxeHMgizjZIQB3TjrS3wItiRO83oj4ZjCGUP5m3IazzZAO0JYWAINDlfED4AhEdwEoRwprdfn8PEXQKzehAZknP3a9ocL3DPxlzIkwFrvIUW2NP6D/KaCcFOmXBnDTZq1DbWKTJzOyAUy0/B6WyWnHGRuB/jkYFwsl7fvugzssfhHJ7E6PG1f3GVXKimQ7hOl49ILUIB4c=" 14 | # OSSRH_JIRA_PASSWORD 15 | - secure: "HjMo4lTx+0OnNUE+0Uug3uaTQd4ruuu2OPncntY+Z4XZ4GqmcjlCX+vGq8QMhWYKgg5Ol7d2MSqYMtHyTyH+6AIbcuouC7PJJHRUAIp+BryStu8W+l6Mt7BT3hwOjlGDi/pVWFJO4BHISQRi36q9I01A8bmwz2sWetdS5EUCictbYpQLv13yczQwzq9954vuA2/+fQLdudi2JxuQbxhl+yGIM4umnaA9ml6ZjAzQd26Eff3bIbPw2/etLBvqTlcfMwTL5IOUqFNEpXh+K38YTCCZh8lCIcA6LsH8EcIgPLPgV4U/+rAwGxhWbKmsct9lyjDfQiCXjLK8znZTVR7hrVBj5a/KXHu+WNR33uk1+Uo7cCT03d1CWp59QKyLW/iT0M6E8muWzb3e0mJBldpWh2qwlqGCgOInf3ofA0/yTf7qUlFNGsU7ZTIRIKj9pp4euF3krxYkx4cn+2BWDjEuUEN3vn8bEETO389dmk64HXWU1MaX7vsyzqxTq9Bzi3co5rIFeEuxBHgSuLWg+cF2RCoSLyg2mpkMGLzUq212y7U5VPWFQ7SvAMXhtH62OW6+QvhLhF6k14BnT73zVvauk1X4IcFGolMxyZSA7P262s1I666E3MY7LZUjZ1HcaWaMDhKLH5QUDhDZrbqNxiRyfsuo82KHv+Ogi8YiHjJeeM8=" 16 | # GPG_KEY_NAME 17 | - secure: "L47DM9hH6CcwWCQ3003FsfdNbnXisz1ulUpeeFcaKK+HJ2j3p1i53OnxdrGJSicgB5Y9qtQ9s43oELt0+wRc0u/kO3iYJ6PSPJW88/NLtowAS+6ChTGXRmc+oO7jc35ObQ4c1g0gPhbw7C7DXFIEbQV31HYIZWeCXfPBbP1CDbGLYra4+WzubOWm+X3MYRUV5w6xf2Cp7FDVz4Uu8kUyCjriAeNEGyDyLdFHPLn0mk8toK+ArBOB1gJXW/4QqE9NnGYoN+GScyYS4HCY9CA0poCLu84rPyLgzDpW4m2sjheFXnQszn+qgquI4PmKneqaxq7nYKJJgyAkYhnWyKfMuk9VqoHbJsvQW0E8aVRmHmlU3gppj5fGLEfzVz6q9chHp4meZBV0dfqpPjmyMxmq1X9fHtlc0yCDoX8lIja2QtRYP4UDThFXzq0z9ZaujFbGQaM4YIdB6NsmToayffYQnidogqHV06ktUA/Q8Q0DnDUXevIEE+/jFXl09pdrJBhD5fLhguAdg2DUxSPzuJrnrN6xD8Rp+K3uEV68iKxeX/hfDq2tClWh77ZJqAiXF3hkcpBklbIF4IYIx7cZTlH7YZ4pPEszYYT8t+hGGOiq6FS7rE1+Y1zjppCg+lenEmrES8GGYQSJDcsFG0g43DEzc9i7o2dGdm31rfQu4XaTtqY=" 18 | # GPG_PASSPHRASE 19 | - secure: "g3sOO7E3L/5t0OT7//qNFzEJ2Wyb6RqlWn4R4KUbd56ZD5DI6X3jm/ojjB/uRd7QSWpO8TpkJgf32I31KUV0aBc6uL4uoN24yq//EYM0LsaILk6ERkoJQzzVu73Ls+CsYFX2Slq1wiSZLgbdtXO/rmgC5dNPMep97UcmgfmfMFRj4IcBcj9OczMyCFtnAIB4TKT2/p5SESwBNLDyJb2nHdfLcrnMIayUQyfhv1XxcTgJzdxMBTHz65Ixjn9j06gp3O1AEFC2jBQLfOH9puQ88j4HANVffd8+5n/B2yDd9OCurZPUiMNpJXzaTyS8Ho1o+4paoQ2jizP3uhoR2l1uzsPtNpCn7Op/MJ0UfTDI7CxeN5dkqYHUuwmPC1Ei68cUAEONUtTapWcmvK2Loegtz8krqyJ/Esox8bnzkSQmdjlp0/ovgia5TumEFqwWd3LYN2Vg3ZMuY6HJdRvWX7gJ1K51qNhN1shIa8UpheEv6e7wsyvFYJ+SL0BrmAir7fI7kDJiZ2oBGu6IYEQ6GXAhmQJ2MLahclCj24qIBZlqdF1DJezPtybU/zlVqWrFtLiRNeqOEW7a9AetaJhF87L0escqPu7GB67Ad5CLJbaMijFOAitV3EAL+U6eA5yo+b2+94wulOcVExFwu32K24kHakm7C0+hkpB+WJVAINnoPNo=" 20 | install: mvn install -P !build-extras -DskipTests=true -Dmaven.javadoc.skip=true -B -V -------------------------------------------------------------------------------- /.travis/before-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if [ "$TRAVIS_PULL_REQUEST" == 'false' ] && [ ! -z "$TRAVIS_TAG" ] 3 | then 4 | openssl aes-256-cbc -K $encrypted_e79fbc6e221e_key -iv $encrypted_e79fbc6e221e_iv -in .travis/codesigning.asc.enc -out .travis/codesigning.asc -d 5 | gpg --fast-import .travis/codesigning.asc 6 | fi -------------------------------------------------------------------------------- /.travis/codesigning.asc.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sputnikdev/bluetooth-manager/7517a25387bb5a5bc75ba793d5f3756a0c41201a/.travis/codesigning.asc.enc -------------------------------------------------------------------------------- /.travis/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | VERSION=$( echo "${TRAVIS_TAG##*-}" ) 3 | if [ "$TRAVIS_PULL_REQUEST" == 'false' ] && [ ! -z "$TRAVIS_TAG" ] 4 | then 5 | echo "on a tag -> set pom.xml to $VERSION" 6 | mvn --settings .travis/settings.xml org.codehaus.mojo:versions-maven-plugin:2.1:set -DnewVersion=$VERSION 1>/dev/null 2>/dev/null 7 | mvn deploy -P sign,build-extras --settings .travis/settings.xml 8 | else 9 | mvn deploy -Dtravis=true -P !build-extras --settings .travis/settings.xml 10 | fi 11 | rc=$? 12 | if [[ $rc -ne 0 ]] ; then 13 | echo 'Could deploy artifact to snaphot/release repository!'; exit $rc 14 | fi -------------------------------------------------------------------------------- /.travis/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ossrh 5 | ${env.OSSRH_JIRA_USERNAME} 6 | ${env.OSSRH_JIRA_PASSWORD} 7 | 8 | 9 | 10 | 11 | 12 | ossrh 13 | 14 | true 15 | 16 | 17 | gpg 18 | ${env.GPG_KEY_NAME} 19 | ${env.GPG_PASSPHRASE} 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/maven-central/v/org.sputnikdev/bluetooth-manager.svg)](https://mvnrepository.com/artifact/org.sputnikdev/bluetooth-manager) 2 | [![Build Status](https://travis-ci.org/sputnikdev/bluetooth-manager.svg?branch=master)](https://travis-ci.org/sputnikdev/bluetooth-manager) 3 | [![Coverage Status](https://coveralls.io/repos/github/sputnikdev/bluetooth-manager/badge.svg?branch=master)](https://coveralls.io/github/sputnikdev/bluetooth-manager?branch=master) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5afbd725e7b24215a350b6d9921a3684)](https://www.codacy.com/app/vkolotov/bluetooth-manager?utm_source=github.com&utm_medium=referral&utm_content=sputnikdev/bluetooth-manager&utm_campaign=Badge_Grade) 5 | [![Join the chat at https://gitter.im/sputnikdev/bluetooth-manager](https://badges.gitter.im/sputnikdev/bluetooth-manager.svg)](https://gitter.im/sputnikdev/bluetooth-manager?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | # bluetooth-manager 7 | Java Bluetooth Manager. A library/framework for managing bluetooth adapters, bluetooth devices, GATT services and characteristics 8 | 9 | The Bluetooth Manager is a set of java APIs which is designed to streamline all the hard work of dealing with unstable 10 | by its nature Bluetooth protocol. 11 | 12 | ## KPIs 13 | 14 | The following KPIs were kept in mind while designing and implementing the Bluetooth Manager: 15 | 16 | 1. Flexibility in using different transports, e.g. serial port, dbus or any other (like tinyb). 17 | 2. Extensibility in adding new supported devices, e.g. different sensors and other hardware. 18 | 3. Robustness. Due to the nature of the Bluetooth protocol the biggest challenge is making the API stable enough 19 | so that end-users could use it. 20 | 4. Comprehensive support for Bluetooth GATT specifications. This is a powerful feature which would allow users: 21 | 1. add any device which conforms GATT specification without developing any custom code 22 | 2. add custom made devices by only specifying a path to a custom defined GATT specification for a device 23 | 24 | ## Implementation overview 25 | 26 | The following diagram outlines some details of the Bluetooth Manager: 27 | ![Bluetooth Manager diagram](bluetooth-manager.png?raw=true "Bluetooth Manager diagram") 28 | 29 | ### Governors API 30 | 31 | The central part of the BM architecture is "Governors" (examples and more info 32 | [BluetoothGovernor](https://github.com/sputnikdev/bluetooth-manager/blob/master/src/main/java/org/sputnikdev/bluetooth/manager/BluetoothGovernor.java), 33 | [AdapterGovernor](https://github.com/sputnikdev/bluetooth-manager/blob/master/src/main/java/org/sputnikdev/bluetooth/manager/AdapterGovernor.java), 34 | [DeviceGovernor](https://github.com/sputnikdev/bluetooth-manager/blob/master/src/main/java/org/sputnikdev/bluetooth/manager/DeviceGovernor.java) and 35 | [BluetoothManager](https://github.com/sputnikdev/bluetooth-manager/blob/master/src/main/java/org/sputnikdev/bluetooth/manager/BluetoothManager.java)). 36 | These are the components which define lifecycle of BT objects and contain logic for error recovery. They are similar to the transport APIs (see below), 37 | but yet different because they are "active" components, i.e. they implement some logic for each of BT objects (adapter, device, characteristic) that make 38 | the system more robust and enable the system to recover from unexpected situations such as disconnections and power outages. 39 | 40 | Apart from making the system stable, the Governors are designed in a such way that they can be used externally, 41 | in other words it is another abstraction layer which hides some specifics/difficulties of the BT protocol behind user-friendly APIs. 42 | 43 | ### Transport API 44 | 45 | A specially designed abstraction layer (transport) is used to bring support 46 | for various bluetooth adapters/dongles, operation systems and hardware architecture types. 47 | 48 | The following diagram outlines some details of the Bluetooth Manager Transport abstraction layer: 49 | ![Transport diagram](bm-transport-abstraction-layer.png?raw=true "Bluetooth Manager Transport abstraction layer") 50 | 51 | There are two implementations of the BT Transport currently: 52 | - [TinyB Transport](https://github.com/sputnikdev/bluetooth-manager-tinyb). 53 | The TinyB transport brings support for: 54 | * Conventional USB bluetooth dongles. 55 | * Linux based operation systems. 56 | * A wide range of hardware architectures (including some ARM based devices, e.g. Raspberry PI etc). 57 | - WIP: [Bluegiga Transport](https://github.com/sputnikdev/bluetooth-manager-bluegiga). 58 | The Bluegiga transport brings support for: 59 | * Bluegiga (BLE112) USB bluetooth dongles. 60 | * Linux, Windows and OSX based operation systems. 61 | * A wide range of hardware architectures (including some ARM based devices, e.g. Raspberry PI etc). 62 | 63 | #### Compatibility matrix 64 | 65 | | | TinyB | Bluegiga | 66 | | :--- | :---: | :---: | 67 | | Windows | - | Y | 68 | | Linux | Y | Y | 69 | | Mac | - | Y | 70 | | X86 32bit | Y | Y | 71 | | X86 64bit | Y | Y | 72 | | ARM v6 (Raspberry PI) | Y | Y | 73 | | Adapters autodiscovery | Y | Y | 74 | | Adapter power management | Y | - | 75 | | Adapter aliases | Y | - | 76 | | BLE devices discovery | Y | Y | 77 | | BR/EDR devices discovery (legacy BT)| Y | - | 78 | 79 | ## Troubleshooting 80 | 81 | * [BlueGiga adapters are not getting discovered](https://github.com/sputnikdev/eclipse-smarthome-bluetooth-binding/issues/6) 82 | * [Generic adapters (TinyB transport) are not getting discovered](https://github.com/sputnikdev/eclipse-smarthome-bluetooth-binding/issues/7) 83 | * [Battery service does not get resolved](https://github.com/sputnikdev/eclipse-smarthome-bluetooth-binding/issues/8) 84 | * [I've added an adapter, but I can't see any bluetooth devices](https://github.com/sputnikdev/eclipse-smarthome-bluetooth-binding/issues/11) 85 | * Nothing happens and I have errors in the log file:
86 | - [GDBus.Error:org.freedesktop.DBus.Error.AccessDenied](https://github.com/sputnikdev/eclipse-smarthome-bluetooth-binding/issues/9) 87 | - [org.sputnikdev.bluetooth.manager.NotReadyException: Could not power adapter](https://github.com/sputnikdev/eclipse-smarthome-bluetooth-binding/issues/10) 88 | 89 | ## Using Bluetooth Manager 90 | 91 | Start using it by specifying maven dependencies from the Maven Central repository: 92 | 93 | ```xml 94 | 95 | org.sputnikdev 96 | bluetooth-manager 97 | X.Y.Z 98 | 99 | 100 | org.sputnikdev 101 | bluetooth-manager-tinyb 102 | X.Y.Z 103 | 104 | ``` 105 | 106 | The example below shows how to set up the Bluetooth Manager and read a characteristic value. 107 | 108 | ### Reading characteristic 109 | 110 | ```java 111 | import org.sputnikdev.bluetooth.URL; 112 | import org.sputnikdev.bluetooth.manager.CharacteristicGovernor; 113 | import org.sputnikdev.bluetooth.manager.impl.BluetoothManagerBuilder; 114 | 115 | public final class BluetoothManagerSimpleTest { 116 | 117 | public static void main(String[] args) throws Exception { 118 | new BluetoothManagerBuilder() 119 | .withTinyBTransport(true) 120 | .withBlueGigaTransport("^*.$") 121 | .build() 122 | .getCharacteristicGovernor(new URL("/XX:XX:XX:XX:XX:XX/F7:EC:62:B9:CF:1F/" 123 | + "0000180f-0000-1000-8000-00805f9b34fb/00002a19-0000-1000-8000-00805f9b34fb"), true) 124 | .whenReady(CharacteristicGovernor::read) 125 | .thenAccept(data -> { 126 | System.out.println("Battery level: " + data[0]); 127 | }).get(); 128 | } 129 | 130 | } 131 | ``` 132 | Here is what happening behind the scene in the example above: 133 | * detecting what adapters are installed in the system (Generic and BlueGiga) 134 | * automatically loading native libraries for the current architecture type (CPU type and OS type) 135 | * setting up bluetooth manager 136 | * choosing the nearest bluetooth adapter 137 | * connecting to the bluetooth device 138 | * resolving GATT services and characteristics 139 | * reading the "Battery Level" characteristic 140 | 141 | ### Receiving characteristic notifications 142 | 143 | ```java 144 | import org.sputnikdev.bluetooth.URL; 145 | import org.sputnikdev.bluetooth.manager.BluetoothManager; 146 | import org.sputnikdev.bluetooth.manager.impl.BluetoothManagerBuilder; 147 | 148 | public class BluetoothManagerSimpleTest { 149 | 150 | public static void main(String args[]) { 151 | // get the Bluetooth Manager 152 | BluetoothManager manager = new BluetoothManagerBuilder() 153 | .withTinyBTransport(true) 154 | .withBlueGigaTransport("^*.$") 155 | .build(); 156 | // define a URL pointing to the target characteristic 157 | URL url = new URL("/88:6B:0F:01:90:CA/CF:FC:9E:B2:0E:63/" + 158 | "0000180f-0000-1000-8000-00805f9b34fb/00002a19-0000-1000-8000-00805f9b34fb"); 159 | // subscribe to the characteristic notification 160 | manager.getCharacteristicGovernor(url, true).addValueListener(value -> { 161 | System.out.println("Battery level: " + value[0]); 162 | }); 163 | // do your other stuff 164 | Thread.sleep(10000); 165 | } 166 | } 167 | ``` 168 | 169 | More examples here: [bluetooth-cli](https://github.com/sputnikdev/bluetooth-cli/tree/master/src/main/java/org/sputnikdev/bluetooth/examples) 170 | 171 | --- 172 | ## Contribution 173 | 174 | You are welcome to contribute to the project, the project environment is designed to make it easy by using: 175 | * Travis CI to release artifacts directly to the Maven Central repository. 176 | * Code style rules to support clarity and supportability. The results can be seen in the Codacy. 177 | * Code coverage reports in the Coveralls to maintain sustainability. 100% of code coverage with unittests is the target. 178 | 179 | The build process is streamlined by using standard maven tools. 180 | 181 | To build the project with maven: 182 | ```bash 183 | mvn clean install 184 | ``` 185 | 186 | To cut a new release and upload it to the Maven Central Repository: 187 | ```bash 188 | mvn release:prepare -B 189 | mvn release:perform 190 | ``` 191 | Travis CI process will take care of everything, you will find a new artifact in the Maven Central repository when the release process finishes successfully. -------------------------------------------------------------------------------- /bluetooth-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sputnikdev/bluetooth-manager/7517a25387bb5a5bc75ba793d5f3756a0c41201a/bluetooth-manager.png -------------------------------------------------------------------------------- /bm-transport-abstraction-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sputnikdev/bluetooth-manager/7517a25387bb5a5bc75ba793d5f3756a0c41201a/bm-transport-abstraction-layer.png -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 102 | 103 | 104 | 106 | 107 | 108 | 109 | 111 | 112 | 113 | 114 | 116 | 117 | 118 | 119 | 120 | 121 | 123 | 124 | 125 | 126 | 128 | 129 | 130 | 131 | 133 | 134 | 135 | 136 | 138 | 140 | 142 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/AdapterDiscoveryListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | 25 | /** 26 | * A listener to handle discovery results for Bluetooth adapters. 27 | * 28 | * @author Vlad Kolotov 29 | */ 30 | @FunctionalInterface 31 | public interface AdapterDiscoveryListener { 32 | 33 | /** 34 | * Method is called when a new Bluetooth adapter gets discovered. 35 | * 36 | * @param adapter a new discovered adapter 37 | */ 38 | void discovered(DiscoveredAdapter adapter); 39 | 40 | /** 41 | * Method is called when a Bluetooth adapter gets lost for any reason, e.g. disconnected from the system. 42 | * 43 | * @param address adapter URL 44 | */ 45 | default void adapterLost(URL address) { } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/AdapterGovernor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.util.List; 24 | 25 | import org.sputnikdev.bluetooth.URL; 26 | 27 | /** 28 | * A governor that manages Bluetooth adapter objects ({@link BluetoothGovernor}). Contains some "offline" and 29 | * "online" methods see {@link BluetoothGovernor}. 30 | * 31 | * @author Vlad Kolotov 32 | */ 33 | public interface AdapterGovernor extends BluetoothGovernor { 34 | 35 | /** 36 | * Returns name of the adapter. 37 | * @return name of the adapter 38 | * @throws NotReadyException if the adapter is not ready 39 | */ 40 | String getName() throws NotReadyException; 41 | 42 | /** 43 | * Returns alias of the adapter. 44 | * @return alias of the adapter 45 | */ 46 | String getAlias() throws NotReadyException; 47 | 48 | /** 49 | * Sets alias for the adapter. 50 | * @param alias new alias 51 | */ 52 | void setAlias(String alias) throws NotReadyException; 53 | 54 | /** 55 | * Returns display name of the adapter. 56 | * @return display name of the adapter 57 | * @throws NotReadyException if the adapter object is not ready 58 | */ 59 | String getDisplayName() throws NotReadyException; 60 | 61 | /** 62 | * Returns adapter powered status. 63 | * @return powered status 64 | * @throws NotReadyException if the adapter object is not ready 65 | */ 66 | boolean isPowered() throws NotReadyException; 67 | 68 | /** 69 | * Returns adapter powered control status. 70 | * @return powered control status 71 | */ 72 | boolean getPoweredControl(); 73 | 74 | /** 75 | * Sets adapter powered control status. 76 | * @param powered a new powered control status 77 | */ 78 | void setPoweredControl(boolean powered); 79 | 80 | /** 81 | * Returns adapter discovering status. 82 | * @return adapter discovering status 83 | * @throws NotReadyException if the adapter object is not ready 84 | */ 85 | boolean isDiscovering() throws NotReadyException; 86 | 87 | /** 88 | * Returns adapter discovering control status. 89 | * @return adapter discovering control status 90 | */ 91 | boolean getDiscoveringControl(); 92 | 93 | /** 94 | * Sets adapter discovering control status. 95 | * @param discovering a new adapter discovering control status 96 | */ 97 | void setDiscoveringControl(boolean discovering); 98 | 99 | /** 100 | * Returns estimated (used defined) signal propagation exponent. It is mainly used in estimated distance 101 | * calculation between the adapter and its devices. This factor is specific to the environment 102 | * where the adapter is used, i.e. how efficient the signal passes through obstacles on its way. 103 | * Normally it ranges from 2.0 (outdoors, no obstacles) to 4.0 (indoors, walls and furniture). 104 | * @return signal propagation exponent 105 | */ 106 | double getSignalPropagationExponent(); 107 | 108 | /** 109 | * Sets estimated (used defined) signal propagation exponent. It is mainly used in estimated distance 110 | * calculation between the adapter and its devices. This factor is specific to the environment 111 | * where the adapter is used, i.e. how efficient the signal passes through obstacles on its way. 112 | * Normally it ranges from 2.0 (outdoors, no obstacles) to 4.0 (indoors, walls and furniture). 113 | * @param exponent signal propagation exponent 114 | */ 115 | void setSignalPropagationExponent(double exponent); 116 | 117 | /** 118 | * Returns a list of discovered Bluetooth devices by the adapter. 119 | * @return a list of discovered Bluetooth devices by the adapter 120 | * @throws NotReadyException if the adapter object is not ready 121 | */ 122 | List getDevices() throws NotReadyException; 123 | 124 | /** 125 | * Returns a list of discovered device governors by the adapter. 126 | * @return a list of discovered device governors by the adapter 127 | * @throws NotReadyException if the adapter object is not ready 128 | */ 129 | List getDeviceGovernors() throws NotReadyException; 130 | 131 | void addAdapterListener(AdapterListener adapterListener); 132 | 133 | void removeAdapterListener(AdapterListener adapterListener); 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/AdapterListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | /** 24 | * A listener to handle bluetooth adapter events. 25 | * 26 | * @author Vlad Kolotov 27 | */ 28 | public interface AdapterListener { 29 | 30 | /** 31 | * Fires when powered status is changed. 32 | * @param powered a new powered status 33 | */ 34 | void powered(boolean powered); 35 | 36 | /** 37 | * Fires when discovering status is changed. 38 | * @param discovering a new discovering status 39 | */ 40 | void discovering(boolean discovering); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothAddressType.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /** 4 | * The fundamental identifier of a Bluetooth® Low-Energy device, similar to an Ethernet or Wi-Fi® Media Access Control 5 | * (MAC) address is the Bluetooth Device Address. This 48-bit (6-byte) number uniquely identifies a device among peers. 6 | * 7 | * @author Vlad Kolotov 8 | */ 9 | public enum BluetoothAddressType { 10 | 11 | /** 12 | * Address type is unknown. 13 | */ 14 | UNKNOWN, 15 | /** 16 | * This is the standard, IEEE-assigned 48-bit universal LAN MAC address which must be obtained from the 17 | * IEEE Registration Authority. 18 | */ 19 | PUBLIC, 20 | /** 21 | * Random type address. BLE standard adds the ability to periodically change the address to insure device privacy. 22 | *

Two random types are provided: 23 | *

    24 | *
  • Static Address. A 48-bit randomly generated address. A new value is generated after each power cycle. 25 | *
  • 26 | *
  • Private Address. When a device wants to remain private, it uses private addresses. These are addresses 27 | * that can be periodically changed so that the device can not be tracked. These may be resolvable or not.
  • 28 | *
29 | */ 30 | RANDOM 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothFatalException.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /** 4 | * An exception that indicates an error that cannot be recovered from by the governor itself 5 | * so that higher level governor (adapter -> device -> characteristic) must take an action (reset itself). 6 | */ 7 | public class BluetoothFatalException extends RuntimeException { 8 | 9 | public BluetoothFatalException() { 10 | 11 | } 12 | 13 | public BluetoothFatalException(String message) { 14 | super(message); 15 | } 16 | 17 | public BluetoothFatalException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothGovernor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | import org.sputnikdev.bluetooth.manager.transport.Adapter; 25 | import org.sputnikdev.bluetooth.manager.transport.Characteristic; 26 | import org.sputnikdev.bluetooth.manager.transport.Device; 27 | 28 | import java.time.Instant; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.function.Consumer; 31 | import java.util.function.Function; 32 | import java.util.function.Predicate; 33 | 34 | /** 35 | * An interface for all Bluetooth governors. Bluetooth governors are the central part of the system. They represent 36 | * different Bluetooth objects such us adapters ({@link Adapter}), 37 | * devices ({@link Device}) and characteristics 38 | * ({@link Characteristic}). By its nature, Bluetooth protocol 39 | * and Bluetooth communication is unstable (devices and adapters can get disconnected or can be out of radio range). 40 | * Therefore the main function of the bluetooth governors is to provide robustness to Bluetooth protocol 41 | * and communication, e.g. once a bluetooth governor is created, it is monitoring and recovering the state of 42 | * a corresponding bluetooth object. Once the state of a corresponding bluetooth object is recovered, 43 | * bluetooth governor changes its status to "ready" ({@link BluetoothGovernor#isReady()}) and fires 44 | * {@link GovernorListener#ready(boolean)} listener. 45 | *
Bluetooth governors provide access to some attributes/properties of their corresponding Bluetooth objects 46 | * through their getter and setter methods. There are two types of getter and setter methods: 47 | *
    48 | *
  • Online methods. These are methods that provide direct access to the bluetooth objects. They expect that 49 | * the corresponding bluetooth object is acquired (ready for use); if the bluetooth object is not ready, then they 50 | * can throw {@link NotReadyException} exception.
  • 51 | *
  • Offline methods. These are methods that set the bluetooth object state which is to be monitored/recovered by 52 | * the Bluetooth Manager. For example, {@link AdapterGovernor#getDiscoveringControl()} 53 | * and {@link AdapterGovernor#setDiscoveringControl(boolean)} are "offline" methods, by setting it to true, 54 | * the Bluetooth Manager will insure that the corresponding adapter is always in "discovering" state. 55 | * The naming convention for this type of methods is setXxxControl and getXxxControl.
  • 56 | *
57 | *
Normally an attribute of a bluetooth object would have three methods: 58 | *
    59 | *
  • an online method (direct access method) to get a status/value of the attribute
  • 60 | *
  • an offline method to set "control state" of the attribute which will be automatically controlled 61 | * (kept in its state) by the Bluetooth Manager
  • 62 | *
  • an offline method to get "control state" of the attribute
  • 63 | *
64 | *
See also org.sputnikdev.bluetooth.manager.impl.BluetoothObjectGovernor for more info about 65 | * internal implementation 66 | * 67 | * @author Vlad Kolotov 68 | */ 69 | public interface BluetoothGovernor { 70 | 71 | /** 72 | * Returns the URL of the corresponding Bluetooth object. 73 | * @return the URL of the corresponding Bluetooth object 74 | */ 75 | URL getURL(); 76 | 77 | /** 78 | * Checks whether the governor is in state when its corresponding bluetooth object is acquired 79 | * and ready for manipulations. 80 | * @return true if the corresponding bluetooth object is acquired and ready for manipulations 81 | */ 82 | boolean isReady(); 83 | 84 | /** 85 | * Returns type of the corresponding Bluetooth object. 86 | * @return type of the corresponding Bluetooth object 87 | */ 88 | BluetoothObjectType getType(); 89 | 90 | /** 91 | * Returns the date/time of last known successful interaction with the corresponding native object. 92 | * @return the date/time of last known successful interaction with the corresponding native object 93 | */ 94 | Instant getLastInteracted(); 95 | 96 | /** 97 | * An accept method of the visitor pattern to process different bluetooth governors at once. 98 | * @param visitor bluetooth governor visitor 99 | * @throws Exception in case of any error 100 | */ 101 | void accept(BluetoothObjectVisitor visitor) throws Exception; 102 | 103 | /** 104 | * Register a new governor listener. 105 | * @param listener a new governor listener 106 | */ 107 | void addGovernorListener(GovernorListener listener); 108 | 109 | /** 110 | * Unregister a governor listener. 111 | * @param listener a governor listener 112 | */ 113 | void removeGovernorListener(GovernorListener listener); 114 | 115 | 116 | CompletableFuture when(Predicate condition, Function function); 117 | 118 | default CompletableFuture doWhen(Predicate predicate, 119 | Consumer consumer) { 120 | return when(predicate, g -> { 121 | consumer.accept((G) this); 122 | return null; 123 | }); 124 | } 125 | 126 | /** 127 | * Returns a completable future that gets completed when governor becomes ready. 128 | * @param function a function that is invoked when governor becomes ready, the completable future is 129 | * completed with the result of this function 130 | * @param bluetooth governor 131 | * @param returned value 132 | * @return a completable future 133 | */ 134 | default CompletableFuture whenReady(Function function) { 135 | return when(BluetoothGovernor::isReady, function); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothInteractionException.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /** 4 | * This exception happens during interactions with bluetooth governors that require a communication 5 | * with physical devices. 6 | * 7 | * @author Vlad Kolotov 8 | */ 9 | public class BluetoothInteractionException extends RuntimeException { 10 | 11 | public BluetoothInteractionException() { } 12 | 13 | public BluetoothInteractionException(String message) { 14 | super(message); 15 | } 16 | 17 | public BluetoothInteractionException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothManager.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | import org.sputnikdev.bluetooth.manager.impl.BluetoothManagerBuilder; 25 | import org.sputnikdev.bluetooth.manager.transport.BluetoothObjectFactory; 26 | 27 | import java.util.Set; 28 | 29 | /** 30 | * The core of the system. Provides various high level methods for accessing bluetooth object governors 31 | * ({@link BluetoothGovernor}) as well as subscribing for bluetooth object discovery events. 32 | * 33 | *

Start using it by accessing a default implementation: 34 | *

 35 |  * {@code
 36 |  * BluetoothManager manager = BluetoothManagerFactory.getManager();
 37 |  * manager.addDiscoveryListener(...);
 38 |  * manager.start(true);
 39 |  * }
 40 |  * 
41 | * 42 | * @author Vlad Kolotov 43 | */ 44 | public interface BluetoothManager { 45 | 46 | /** 47 | * Starts bluetooth manager background activities. 48 | * 49 | *

If true is provided for the argument, then bluetooth manager will activate bluetooth device discovery 50 | * process for all available bluetooth adapters. Discovered bluetooth adapters and devices will be available: 51 | *

    52 | *
  • By executing {@link BluetoothManager#getDiscoveredDevices()}
  • 53 | *
  • By listening to discovery events via {@link DeviceDiscoveryListener}
  • 54 | *
55 | * 56 | *

If false is provided for the argument, then bluetooth manager won't activate device discovery process. 57 | * However, it is possible to activate device discovery process for a particular bluetooth adapter 58 | * if its MAC address is known: 59 | *

 60 |      * {@code
 61 |      *
 62 |      * BluetoothManager manager = BluetoothManagerFactory.getManager();
 63 |      * manager.addDiscoveryListener(...);
 64 |      * manager.start(false);
 65 |      * manager.getAdapterGovernor(new URL("/XX:XX:XX:XX:XX:XX")).setDiscoveringControl(true);
 66 |      * }
 67 |      * 
68 | * 69 | * @param startDiscovering controls whether bluetooth manager should start bluetooth device discovery 70 | */ 71 | void start(boolean startDiscovering); 72 | 73 | /** 74 | * Shuts down all bluetooth manager background activities. 75 | */ 76 | void stop(); 77 | 78 | /** 79 | * Checks whether the bluetooth manager has been started. 80 | * @return true if started, false otherwise 81 | */ 82 | boolean isStarted(); 83 | 84 | /** 85 | * Register a new device discovery listener. 86 | * 87 | * @param deviceDiscoveryListener a new device discovery listener 88 | */ 89 | void addDeviceDiscoveryListener(DeviceDiscoveryListener deviceDiscoveryListener); 90 | 91 | /** 92 | * Unregisters a device discovery listener. 93 | * @param deviceDiscoveryListener a device discovery listener 94 | */ 95 | void removeDeviceDiscoveryListener(DeviceDiscoveryListener deviceDiscoveryListener); 96 | 97 | /** 98 | * Register a new adapter discovery listener. 99 | * 100 | * @param adapterDiscoveryListener a new device discovery listener 101 | */ 102 | void addAdapterDiscoveryListener(AdapterDiscoveryListener adapterDiscoveryListener); 103 | 104 | /** 105 | * Unregisters a adapter discovery listener. 106 | * @param adapterDiscoveryListener a device discovery listener 107 | */ 108 | void removeAdapterDiscoveryListener(AdapterDiscoveryListener adapterDiscoveryListener); 109 | 110 | /** 111 | * Return a list of discovered bluetooth devices. 112 | * @return a list of discovered bluetooth devices 113 | */ 114 | Set getDiscoveredDevices(); 115 | 116 | /** 117 | * Return a list of discovered bluetooth adapters. 118 | * @return a list of discovered bluetooth adapters 119 | */ 120 | Set getDiscoveredAdapters(); 121 | 122 | /** 123 | * Creates a new bluetooth governor or returns an existing one by its URL. 124 | * 125 | * @param url a URL of a bluetooth object (adapter, device, characteristic) 126 | * @return a bluetooth governor 127 | */ 128 | BluetoothGovernor getGovernor(URL url); 129 | 130 | /** 131 | * Creates a new adapter governor or returns an existing one by its URL. 132 | * @param url a URL of a bluetooth adapter 133 | * @return an adapter governor 134 | */ 135 | AdapterGovernor getAdapterGovernor(URL url); 136 | 137 | /** 138 | * Creates a new device governor or returns an existing one by its URL. 139 | * @param url a URL of a bluetooth device 140 | * @return an device governor 141 | */ 142 | DeviceGovernor getDeviceGovernor(URL url); 143 | 144 | /** 145 | * Creates a new device governor or returns an existing one by its URL. 146 | * If the provided boolean argument is set to true, then the connection control 147 | * ({@link DeviceGovernor#setConnectionControl(boolean)}) is enabled for the governor. 148 | * @param url a URL of a bluetooth device 149 | * @param forceConnect if set to true, governor is forced to establish connection to the device 150 | * @return an device governor 151 | */ 152 | DeviceGovernor getDeviceGovernor(URL url, boolean forceConnect); 153 | 154 | /** 155 | * Creates a new characteristic governor or returns an existing one by its URL. 156 | * @param url a URL of a bluetooth characteristic 157 | * @return a characteristic governor 158 | */ 159 | CharacteristicGovernor getCharacteristicGovernor(URL url); 160 | 161 | /** 162 | * Creates a new characteristic governor or returns an existing one by its URL. 163 | * If the provided boolean argument is set to true, then the connection control 164 | * ({@link DeviceGovernor#setConnectionControl(boolean)}) is enabled for the corresponding governor. 165 | * @param url a URL of a bluetooth characteristic 166 | * @param forceConnect if set to true, the corresponding DeviceGovernor connection control is set to true 167 | * @return a characteristic governor 168 | */ 169 | CharacteristicGovernor getCharacteristicGovernor(URL url, boolean forceConnect); 170 | 171 | /** 172 | * Disposes/ shuts down the bluetooth manager and its governors. 173 | */ 174 | void dispose(); 175 | 176 | /** 177 | * Checks whether the bluetooth manager is in the "combine adapters" mode 178 | * ({@link BluetoothManagerBuilder#withCombinedAdapters(boolean)}. 179 | * @return true if the "combined adapters" mode is enabled 180 | */ 181 | boolean isCombinedAdaptersEnabled(); 182 | 183 | /** 184 | * Checks whether the bluetooth manager is in the "combine devices" mode 185 | * ({@link BluetoothManagerBuilder#withCombinedDevices(boolean)}}). 186 | * @return true if the "combined devices" mode is enabled 187 | */ 188 | boolean isCombinedDevicesEnabled(); 189 | 190 | /** 191 | * Adds a new bluetooth manager listener. 192 | * @param listener a manager listener 193 | */ 194 | void addManagerListener(ManagerListener listener); 195 | 196 | /** 197 | * Removes an existing bluetooth manager listener. 198 | * @param listener an existing bluetooth manager listener 199 | */ 200 | void removeManagerListener(ManagerListener listener); 201 | 202 | /** 203 | * Registers a new Bluetooth Object factory (transport). 204 | * @param transport a new Bluetooth Object factory 205 | */ 206 | void registerFactory(BluetoothObjectFactory transport); 207 | 208 | /** 209 | * Un-registers a previously registered Bluetooth Object factory (transport). 210 | * @param transport a Bluetooth Object factory 211 | */ 212 | void unregisterFactory(BluetoothObjectFactory transport); 213 | 214 | /** 215 | * Returns the refresh rate of how often bluetooth devices are checked/updated. 216 | * @return refresh rate 217 | */ 218 | int getRefreshRate(); 219 | 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothObjectType.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | 24 | /** 25 | * Bluetooth object types enum. 26 | * @author Vlad Kolotov 27 | */ 28 | public enum BluetoothObjectType { 29 | 30 | ADAPTER, 31 | DEVICE, 32 | CHARACTERISTIC 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothObjectVisitor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | 24 | /** 25 | * A helper interface to handle different bluetooth objects in a generic manner. It is a part of visitor pattern 26 | * (see {@link BluetoothGovernor#accept(BluetoothObjectVisitor)}). 27 | * 28 | * @author Vlad Kolotov 29 | */ 30 | public interface BluetoothObjectVisitor { 31 | 32 | void visit(AdapterGovernor governor) throws Exception; 33 | 34 | void visit(DeviceGovernor governor) throws Exception; 35 | 36 | void visit(CharacteristicGovernor governor) throws Exception; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/BluetoothSmartDeviceListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | 29 | /** 30 | * A listener of events for BLE devices. 31 | * 32 | * @author Vlad Kolotov 33 | */ 34 | @FunctionalInterface 35 | public interface BluetoothSmartDeviceListener { 36 | 37 | /** 38 | * Fires when the device gets connected. 39 | */ 40 | default void connected() { } 41 | 42 | /** 43 | * Fires when the device gets disconnected. 44 | */ 45 | default void disconnected() { } 46 | 47 | /** 48 | * Fires when GATT services get resolved. 49 | * 50 | * @param gattServices a list of resolved GATT services 51 | */ 52 | void servicesResolved(List gattServices); 53 | 54 | /** 55 | * Fires when GATT services get unresolved. 56 | */ 57 | default void servicesUnresolved() { } 58 | 59 | /** 60 | * Fires when the device advertises service data. The key is service UUID (16, 32 or 128 bit), 61 | * the value is advertised data. 62 | * @param serviceData service data 63 | */ 64 | default void serviceDataChanged(Map serviceData) { } 65 | 66 | /** 67 | * Fires when the device advertises manufacturer data. The key is manufacturer ID, the value is advertised data. 68 | * @param manufacturerData manufacturer data 69 | */ 70 | default void manufacturerDataChanged(Map manufacturerData) { } 71 | 72 | default void authenticated() { } 73 | 74 | default void authenticationFailure(Exception reason) { } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/CharacteristicGovernor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType; 24 | 25 | import java.time.Instant; 26 | import java.util.Set; 27 | import java.util.concurrent.CompletableFuture; 28 | import java.util.function.Consumer; 29 | import java.util.function.Function; 30 | 31 | /** 32 | * Bluetooth characteristic governor ({@link BluetoothGovernor}). 33 | * 34 | * @author Vlad Kolotov 35 | */ 36 | public interface CharacteristicGovernor extends BluetoothGovernor { 37 | 38 | /** 39 | * Returns access types (flags) supported by the characteristic. 40 | * 41 | * @return flags supported by the characteristic 42 | * @throws NotReadyException if the bluetooth object is not ready 43 | */ 44 | Set getFlags() throws NotReadyException; 45 | 46 | /** 47 | * Checks whether the characteristic can be read. 48 | * 49 | * @return true if the characteristic can be read, otherwise false 50 | * @throws NotReadyException if the bluetooth object is not ready 51 | */ 52 | boolean isReadable() throws NotReadyException; 53 | 54 | /** 55 | * Checks whether the characteristic can be written. 56 | * 57 | * @return true if the characteristic can be written, otherwise false 58 | * @throws NotReadyException if the bluetooth object is not ready 59 | */ 60 | boolean isWritable() throws NotReadyException; 61 | 62 | /** 63 | * Checks whether the characteristic can notify. 64 | * 65 | * @return true if the characteristic can notify, otherwise false 66 | * @throws NotReadyException if the bluetooth object is not ready 67 | */ 68 | boolean isNotifiable() throws NotReadyException; 69 | 70 | 71 | /** 72 | * Return true if notification is enabled, false otherwise. 73 | * @return true if notification is enabled, false otherwise 74 | * @throws NotReadyException if the bluetooth object is not ready 75 | */ 76 | boolean isNotifying() throws NotReadyException; 77 | 78 | /** 79 | * Reads state from the characteristic. 80 | * 81 | * @return characteristic state 82 | * @throws NotReadyException if the bluetooth object is not ready 83 | */ 84 | byte[] read() throws NotReadyException; 85 | 86 | /** 87 | * Writes state to the characteristic. 88 | * @param data a new characteristic state 89 | * @return true if the new state is written 90 | * @throws NotReadyException if the bluetooth object is not ready 91 | */ 92 | boolean write(byte[] data) throws NotReadyException; 93 | 94 | /** 95 | * Register a new characteristic listener. 96 | * @param valueListener new characteristic listener 97 | */ 98 | void addValueListener(ValueListener valueListener); 99 | 100 | /** 101 | * Removes a previously registered characteristic listener. 102 | * @param valueListener a previously registered characteristic listener 103 | */ 104 | void removeValueListener(ValueListener valueListener); 105 | 106 | /** 107 | * Returns the date/time of last known received notification. 108 | * @return the date/time of last known received notification 109 | */ 110 | Instant getLastNotified(); 111 | 112 | boolean isAuthenticated(); 113 | 114 | 115 | default CompletableFuture whenAuthenticated(Function function) { 116 | return when(CharacteristicGovernor::isAuthenticated, function); 117 | } 118 | 119 | default CompletableFuture whenAuthenticatedThanDo(Consumer consumer) { 120 | return whenAuthenticated(g -> { 121 | consumer.accept((G) this); 122 | return null; 123 | }); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/CombinedDeviceGovernor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | import org.sputnikdev.bluetooth.URL; 4 | 5 | /** 6 | * 7 | * @author Vlad Kolotov 8 | */ 9 | public interface CombinedDeviceGovernor extends CombinedGovernor { 10 | 11 | /** 12 | * Sets connection strategy for the combined device governor. 13 | * @param strategy a connection strategy 14 | */ 15 | void setConnectionStrategy(ConnectionStrategy strategy); 16 | 17 | /** 18 | * Returns the connection strategy of the combined device governor. 19 | * @return the connection strategy 20 | */ 21 | ConnectionStrategy getConnectionStrategy(); 22 | 23 | /** 24 | * Sets an adapter URL that is a preferred adapter to be connected to. 25 | * @param adapter a preferred adapter 26 | */ 27 | void setPreferredAdapter(URL adapter); 28 | 29 | /** 30 | * Returns the preferred adapter of the combined device governor. 31 | * @return the preferred adapter 32 | */ 33 | URL getPreferredAdapter(); 34 | 35 | /** 36 | * Returns the URL of an adapter the device is connected to. If the device is not connected, then the result is null. 37 | * @return URL of an adapter the device is connected to 38 | */ 39 | URL getConnectedAdapter(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/CombinedGovernor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | public interface CombinedGovernor { 4 | 5 | String COMBINED_ADDRESS = "XX:XX:XX:XX:XX:XX"; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/ConnectionStrategy.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | public enum ConnectionStrategy { 4 | 5 | NEAREST_ADAPTER, 6 | PREFERRED_ADAPTER 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/DeviceDiscoveryListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | 25 | 26 | /** 27 | * A listener of discovery events. 28 | * 29 | * @author Vlad Kolotov 30 | */ 31 | @FunctionalInterface 32 | public interface DeviceDiscoveryListener { 33 | 34 | /** 35 | * Fires when a new bluetooth adapter or bluetooth device gets discovered. 36 | * 37 | * @param discoveredDevice a new discovered bluetooth adapter or bluetooth device 38 | */ 39 | void discovered(DiscoveredDevice discoveredDevice); 40 | 41 | /** 42 | * Fires when a bluetooth adapter or a bluetooth device gets lost. 43 | * 44 | * @param lostDevice device that has been lost 45 | */ 46 | default void deviceLost(DiscoveredDevice lostDevice) { } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/DiscoveredAdapter.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | 25 | /** 26 | * Objects of this class capture discovery results for Bluetooth adapters. 27 | * 28 | * @author Vlad Kolotov 29 | */ 30 | public class DiscoveredAdapter implements DiscoveredObject { 31 | 32 | private static final String COMBINED_ADAPTER_ADDRESS = CombinedGovernor.COMBINED_ADDRESS; 33 | 34 | private final URL url; 35 | private final String name; 36 | private final String alias; 37 | 38 | /** 39 | * Creates a new object. 40 | * @param url bluetooth object URL 41 | * @param name bluetooth object name 42 | * @param alias bluetooth object alias 43 | */ 44 | public DiscoveredAdapter(URL url, String name, String alias) { 45 | this.url = url; 46 | this.name = name; 47 | this.alias = alias; 48 | } 49 | 50 | /** 51 | * Returns bluetooth object URL. 52 | * @return bluetooth object URL 53 | */ 54 | @Override 55 | public URL getURL() { 56 | return url; 57 | } 58 | 59 | /** 60 | * Returns bluetooth object name. 61 | * @return bluetooth object name 62 | */ 63 | @Override 64 | public String getName() { 65 | return name; 66 | } 67 | 68 | /** 69 | * Returns bluetooth object alias. 70 | * @return bluetooth object alias 71 | */ 72 | @Override 73 | public String getAlias() { 74 | return alias; 75 | } 76 | 77 | @Override 78 | public boolean isCombined() { 79 | return COMBINED_ADAPTER_ADDRESS.equalsIgnoreCase(url.getAdapterAddress()); 80 | } 81 | 82 | @Override 83 | public boolean equals(Object object) { 84 | if (this == object) { 85 | return true; 86 | } 87 | if (object == null || getClass() != object.getClass()) { 88 | return false; 89 | } 90 | 91 | DiscoveredAdapter that = (DiscoveredAdapter) object; 92 | return url.equals(that.url); 93 | 94 | } 95 | 96 | @Override 97 | public int hashCode() { 98 | int result = url.hashCode(); 99 | result = 31 * result + url.hashCode(); 100 | return result; 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | String displayName = getDisplayName(); 106 | return "[Adapter] " + getURL() + " [" + displayName + "]"; 107 | } 108 | 109 | @Override 110 | public String getDisplayName() { 111 | return alias != null ? alias : name; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/DiscoveredDevice.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | 25 | import java.util.Objects; 26 | 27 | 28 | /** 29 | * Objects of this class capture discovery results for Bluetooth devices. 30 | * 31 | * @author Vlad Kolotov 32 | */ 33 | public class DiscoveredDevice implements DiscoveredObject { 34 | 35 | private static final String COMBINED_DEVICE_ADDRESS = CombinedGovernor.COMBINED_ADDRESS; 36 | 37 | private final URL url; 38 | private final String name; 39 | private final String alias; 40 | private short rssi; 41 | private int bluetoothClass; 42 | private boolean bleEnabled; 43 | 44 | /** 45 | * Creates a new instance based on previously created object. 46 | * @param device device to copy 47 | */ 48 | public DiscoveredDevice(DiscoveredDevice device) { 49 | this(device.getURL(), device.getName(), device.getAlias(), device.getRSSI(), 50 | device.getBluetoothClass(), device.isBleEnabled()); 51 | } 52 | 53 | /** 54 | * Creates a new object. 55 | * @param url bluetooth object URL 56 | * @param name bluetooth object name 57 | * @param alias bluetooth object alias 58 | * @param rssi bluetooth object RSSI 59 | * @param bluetoothClass bluetooth object class 60 | * @param bleEnabled indicated if it is a BLE enabled device 61 | */ 62 | public DiscoveredDevice(URL url, String name, String alias, short rssi, int bluetoothClass, boolean bleEnabled) { 63 | this.url = url; 64 | this.name = name; 65 | this.alias = alias; 66 | this.rssi = rssi; 67 | this.bluetoothClass = bluetoothClass; 68 | this.bleEnabled = bleEnabled; 69 | } 70 | 71 | /** 72 | * Returns bluetooth object URL. 73 | * @return bluetooth object URL 74 | */ 75 | @Override 76 | public URL getURL() { 77 | return url; 78 | } 79 | 80 | /** 81 | * Returns bluetooth object name. 82 | * @return bluetooth object name 83 | */ 84 | @Override 85 | public String getName() { 86 | return name; 87 | } 88 | 89 | /** 90 | * Returns bluetooth object alias. 91 | * @return bluetooth object alias 92 | */ 93 | @Override 94 | public String getAlias() { 95 | return alias; 96 | } 97 | 98 | /** 99 | * Returns bluetooth object RSSI. 100 | * @return bluetooth object RSSI 101 | */ 102 | public short getRSSI() { 103 | return rssi; 104 | } 105 | 106 | /** 107 | * Returns bluetooth object class. 108 | * @return bluetooth object class 109 | */ 110 | public int getBluetoothClass() { 111 | return bluetoothClass; 112 | } 113 | 114 | /** 115 | * Indicates if this device is Bluetooth Low Energy enabled device. 116 | * @return true if Bluetooth Low Energy enabled device, false otherwise 117 | */ 118 | public boolean isBleEnabled() { 119 | return bleEnabled; 120 | } 121 | 122 | @Override 123 | public boolean isCombined() { 124 | return COMBINED_DEVICE_ADDRESS.equalsIgnoreCase(url.getAdapterAddress()); 125 | } 126 | 127 | @Override 128 | public boolean equals(Object o) { 129 | if (this == o) { 130 | return true; 131 | } 132 | if (!(o instanceof DiscoveredDevice)) { 133 | return false; 134 | } 135 | DiscoveredDevice that = (DiscoveredDevice) o; 136 | return Objects.equals(url, that.url); 137 | } 138 | 139 | @Override 140 | public int hashCode() { 141 | return Objects.hash(url); 142 | } 143 | 144 | @Override 145 | public String toString() { 146 | String displayName = getDisplayName(); 147 | return "[Device] " + getURL() + " [" + displayName + "]"; 148 | } 149 | 150 | @Override 151 | public String getDisplayName() { 152 | return alias != null ? alias : name; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/DiscoveredObject.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | import org.sputnikdev.bluetooth.URL; 4 | 5 | /** 6 | * Root interface for all bluetooth objects discovered by Bluetooth Manager. 7 | * @author Vlad Kolotov 8 | */ 9 | public interface DiscoveredObject { 10 | 11 | /** 12 | * Returns bluetooth object URL. 13 | * @return bluetooth object URL 14 | */ 15 | URL getURL(); 16 | 17 | /** 18 | * Returns bluetooth object name. 19 | * @return bluetooth object name 20 | */ 21 | String getName(); 22 | 23 | /** 24 | * Returns bluetooth object alias. 25 | * @return bluetooth object alias 26 | */ 27 | String getAlias(); 28 | 29 | /** 30 | * Returns bluetooth object display name. 31 | * @return bluetooth object display name 32 | */ 33 | String getDisplayName(); 34 | 35 | /** 36 | * Checks whether this discovery result represents a set of objects. 37 | * @return true if the object represents a set of objects 38 | */ 39 | boolean isCombined(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/GattCharacteristic.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | import org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType; 25 | 26 | import java.util.Collections; 27 | import java.util.Set; 28 | 29 | /** 30 | * A class to capture discovered GATT characteristics. 31 | * 32 | * @author Vlad Kolotov 33 | */ 34 | public class GattCharacteristic { 35 | 36 | private final URL url; 37 | private final Set flags; 38 | 39 | /** 40 | * Creates a new object. 41 | * @param url characteristic URL 42 | * @param flags characteristic access flags 43 | */ 44 | public GattCharacteristic(URL url, Set flags) { 45 | this.url = url; 46 | this.flags = flags; 47 | } 48 | 49 | /** 50 | * Returns characteristic URL. 51 | * @return characteristic URL 52 | */ 53 | public URL getURL() { 54 | return url; 55 | } 56 | 57 | /** 58 | * Returns characteristic access flags. 59 | * @return characteristic access flags 60 | */ 61 | public Set getFlags() { 62 | return Collections.unmodifiableSet(flags); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/GattService.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | 25 | import java.util.Collections; 26 | import java.util.List; 27 | 28 | /** 29 | * A class to capture discovered GATT services. 30 | * 31 | * @author Vlad Kolotov 32 | */ 33 | public class GattService { 34 | 35 | private final URL uuid; 36 | private final List characteristics; 37 | 38 | /** 39 | * Create a new object. 40 | * @param uuid service URL 41 | * @param characteristics a list of service characteristics 42 | */ 43 | public GattService(URL uuid, List characteristics) { 44 | this.uuid = uuid; 45 | this.characteristics = Collections.unmodifiableList(characteristics); 46 | } 47 | 48 | /** 49 | * Returns service URL. 50 | * @return service URL 51 | */ 52 | public URL getURL() { 53 | return uuid; 54 | } 55 | 56 | /** 57 | * Returns characteristics list of the service. 58 | * @return characteristics list of the service 59 | */ 60 | public List getCharacteristics() { 61 | return characteristics; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/GenericBluetoothDeviceListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | /** 24 | * 25 | * A listener of events for generic bluetooth devices. 26 | * 27 | * @author Vlad Kolotov 28 | */ 29 | public interface GenericBluetoothDeviceListener { 30 | 31 | /** 32 | * Fires when a device gets online. 33 | */ 34 | void online(); 35 | 36 | /** 37 | * Fires then a device gets offline. 38 | */ 39 | void offline(); 40 | 41 | /** 42 | * Fires when a device gets blocked/unblocked. 43 | * @param blocked is true when device get blocked, false otherwise 44 | */ 45 | void blocked(boolean blocked); 46 | 47 | /** 48 | * Reports a device RSSI level. 49 | * @param rssi a device RSSI level 50 | */ 51 | void rssiChanged(short rssi); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/GovernorListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | import java.time.Instant; 4 | 5 | /*- 6 | * #%L 7 | * org.sputnikdev:bluetooth-manager 8 | * %% 9 | * Copyright (C) 2017 Sputnik Dev 10 | * %% 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | * #L% 23 | */ 24 | 25 | 26 | /** 27 | * 28 | * A listener to watch governors events. 29 | * 30 | */ 31 | @FunctionalInterface 32 | public interface GovernorListener { 33 | 34 | /** 35 | * Reports when a device/governor changes its status. See {@link BluetoothGovernor} for more info. 36 | * @param isReady true if a device/adapter becomes ready for interactions (hardware acquired), false otherwise 37 | */ 38 | void ready(boolean isReady); 39 | 40 | /** 41 | * Reports when a device/governor was last active (receiving events, sending commands etc). 42 | * @param lastActivity a date when a device was last active 43 | */ 44 | default void lastUpdatedChanged(Instant lastActivity) { } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/GovernorState.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | public enum GovernorState { 4 | 5 | NEW, 6 | READY, 7 | RESET, 8 | DISPOSED 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/ManagerListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /** 4 | * A listener that is used to subscribe for some internal bluetooth manager events. 5 | */ 6 | @FunctionalInterface 7 | public interface ManagerListener { 8 | 9 | /** 10 | * Notifies when a governor becomes ready or otherwise. 11 | * @param governor a bluetooth governor 12 | * @param ready true if ready, false otherwise 13 | */ 14 | void ready(BluetoothGovernor governor, boolean ready); 15 | 16 | /** 17 | * Notifies when the bluetooth manager gets disposed. 18 | */ 19 | default void disposed() { } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/NotReadyException.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | 24 | /** 25 | * 26 | * An exception class is used to signal that some operations could not be done due to device/governor state. 27 | * It this exception occurs, this means that a corresponding to a governor low level object is still not acquired. 28 | * See {@link BluetoothGovernor} for more info. 29 | * 30 | * @author Vlad Kolotov 31 | */ 32 | public class NotReadyException extends RuntimeException { 33 | 34 | /** 35 | * A constructor without message. 36 | */ 37 | public NotReadyException() { 38 | super(); 39 | } 40 | 41 | /** 42 | * A constructor with a message. 43 | * @param message a message 44 | */ 45 | public NotReadyException(String message) { 46 | super(message); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/ValueListener.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | 24 | /** 25 | * 26 | * A value change listener. Mainly is used within a characteristic governor. 27 | * 28 | * @author Vlad Kolotov 29 | */ 30 | public interface ValueListener { 31 | 32 | /** 33 | * Reports value changed event. 34 | * @param value a new state 35 | */ 36 | void changed(byte[] value); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/auth/AuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.auth; 2 | 3 | import org.sputnikdev.bluetooth.manager.BluetoothManager; 4 | import org.sputnikdev.bluetooth.manager.DeviceGovernor; 5 | 6 | @FunctionalInterface 7 | public interface AuthenticationProvider { 8 | 9 | void authenticate(BluetoothManager bluetoothManager, DeviceGovernor governor); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/auth/BluetoothAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.auth; 2 | 3 | public class BluetoothAuthenticationException extends RuntimeException { 4 | 5 | public BluetoothAuthenticationException() { } 6 | 7 | public BluetoothAuthenticationException(String message) { 8 | super(message); 9 | } 10 | 11 | public BluetoothAuthenticationException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/auth/PinCodeAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.auth; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.sputnikdev.bluetooth.URL; 6 | import org.sputnikdev.bluetooth.manager.BluetoothManager; 7 | import org.sputnikdev.bluetooth.manager.CharacteristicGovernor; 8 | import org.sputnikdev.bluetooth.manager.DeviceGovernor; 9 | import org.sputnikdev.bluetooth.manager.ValueListener; 10 | 11 | import java.util.Arrays; 12 | import java.util.concurrent.CompletableFuture; 13 | import java.util.concurrent.CountDownLatch; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.TimeoutException; 16 | import java.util.concurrent.locks.ReentrantLock; 17 | 18 | public class PinCodeAuthenticationProvider implements AuthenticationProvider { 19 | 20 | private final Logger logger = LoggerFactory.getLogger(PinCodeAuthenticationProvider.class); 21 | 22 | private final String pinCodeServiceUUID; 23 | private final String pinCodeCharacteristicUUID; 24 | private byte[] pinCode; 25 | private byte[] expectedAuthenticationResponse; 26 | private CountDownLatch authResponseLatch; 27 | private byte[] authResponse; 28 | private final ValueListener authListener = data -> { 29 | authResponse = data; 30 | if (authResponseLatch != null) { 31 | authResponseLatch.countDown(); 32 | } 33 | }; 34 | private CompletableFuture authFuture; 35 | private final ReentrantLock lock = new ReentrantLock(); 36 | 37 | public PinCodeAuthenticationProvider(String pinCodeServiceUUID, String pinCodeCharacteristicUUID) { 38 | this.pinCodeServiceUUID = pinCodeServiceUUID; 39 | this.pinCodeCharacteristicUUID = pinCodeCharacteristicUUID; 40 | } 41 | 42 | public PinCodeAuthenticationProvider(String pinCodeServiceUUID, String pinCodeCharacteristicUUID, byte[] pinCode) { 43 | this.pinCodeServiceUUID = pinCodeServiceUUID; 44 | this.pinCodeCharacteristicUUID = pinCodeCharacteristicUUID; 45 | this.pinCode = Arrays.copyOf(pinCode, pinCode.length); 46 | } 47 | 48 | public PinCodeAuthenticationProvider(String pinCodeServiceUUID, String pinCodeCharacteristicUUID, 49 | byte[] pinCode, byte[] expectedAuthenticationResponse) { 50 | this.pinCodeServiceUUID = pinCodeServiceUUID; 51 | this.pinCodeCharacteristicUUID = pinCodeCharacteristicUUID; 52 | this.pinCode = Arrays.copyOf(pinCode, pinCode.length); 53 | if (expectedAuthenticationResponse != null) { 54 | this.expectedAuthenticationResponse = 55 | Arrays.copyOf(expectedAuthenticationResponse, expectedAuthenticationResponse.length); 56 | } 57 | } 58 | 59 | public String getPinCodeServiceUUID() { 60 | return pinCodeServiceUUID; 61 | } 62 | 63 | public String getPinCodeCharacteristicUUID() { 64 | return pinCodeCharacteristicUUID; 65 | } 66 | 67 | public byte[] getPinCode() { 68 | return Arrays.copyOf(pinCode, pinCode.length); 69 | } 70 | 71 | public void setPinCode(byte[] pinCode) { 72 | this.pinCode = Arrays.copyOf(pinCode, pinCode.length); 73 | } 74 | 75 | public byte[] getExpectedAuthenticationResponse() { 76 | if (expectedAuthenticationResponse == null) { 77 | return null; 78 | } 79 | return Arrays.copyOf(expectedAuthenticationResponse, expectedAuthenticationResponse.length); 80 | } 81 | 82 | public void setExpectedAuthenticationResponse(byte[] expectedAuthenticationResponse) { 83 | if (expectedAuthenticationResponse == null) { 84 | this.expectedAuthenticationResponse = null; 85 | } else { 86 | this.expectedAuthenticationResponse = 87 | Arrays.copyOf(expectedAuthenticationResponse, expectedAuthenticationResponse.length); 88 | } 89 | } 90 | 91 | @Override 92 | public void authenticate(BluetoothManager bluetoothManager, DeviceGovernor governor) 93 | throws BluetoothAuthenticationException { 94 | if (lock.tryLock()) { 95 | try { 96 | URL pinCodeURL = governor.getURL().copyWith(pinCodeServiceUUID, pinCodeCharacteristicUUID); 97 | int timeout = bluetoothManager.getRefreshRate() * 2; 98 | CharacteristicGovernor pinCodeChar = bluetoothManager.getCharacteristicGovernor(pinCodeURL); 99 | // we are enabling notification anyway, 100 | // some devices require it to be enabled even if there is not any expected auth result 101 | pinCodeChar.addValueListener(authListener); 102 | try { 103 | logger.debug("Commencing authentication procedure: {}", pinCodeURL); 104 | authFuture = pinCodeChar.doWhen(this::readyForAuthentication, gov -> { 105 | performAuthentication(gov, timeout); 106 | }); 107 | authFuture.get(timeout, TimeUnit.SECONDS); 108 | } catch (TimeoutException e) { 109 | throw new BluetoothAuthenticationException("Could not authenticate. Timeout: " + pinCodeURL, e); 110 | } catch (Exception ex) { 111 | throw new BluetoothAuthenticationException("Could not authenticate: " 112 | + pinCodeURL + "; Error: " + ex.getMessage(), ex); 113 | } finally { 114 | if (authFuture != null && !authFuture.isDone()) { 115 | authFuture.cancel(true); 116 | authFuture = null; 117 | } 118 | } 119 | } finally { 120 | lock.unlock(); 121 | } 122 | } else { 123 | logger.warn("Authentication procedure has already been commenced. Skipping this time: {}", 124 | governor.getURL()); 125 | } 126 | } 127 | 128 | void performAuthentication(CharacteristicGovernor pinCodeChar, int timeout) { 129 | URL pinCodeURL = pinCodeChar.getURL(); 130 | if (expectedAuthenticationResponse != null) { 131 | logger.debug("Performing complex authentication with response verification: {}", pinCodeURL); 132 | 133 | if (!pinCodeChar.isNotifiable()) { 134 | throw new IllegalStateException("Complex authentication requested, " 135 | + "but the authentication characteristic " 136 | + "does not support notifications: " + pinCodeURL); 137 | } 138 | 139 | logger.debug("Sending pin code to device: {} ", pinCodeURL); 140 | authResponseLatch = new CountDownLatch(1); 141 | if (!pinCodeChar.write(pinCode)) { 142 | throw new BluetoothAuthenticationException("Could not send pin code: " + pinCodeURL); 143 | } 144 | try { 145 | logger.debug("Waiting for a response from the authentication characteristic: {}", pinCodeURL); 146 | authResponseLatch.await(timeout, TimeUnit.SECONDS); 147 | } catch (InterruptedException ignore) { /* ignore */ } 148 | if (authResponse != null) { 149 | logger.debug("Authentication response has been received. Checking if it matches to the " 150 | + "expected response: {}", pinCodeURL); 151 | if (!Arrays.equals(expectedAuthenticationResponse, authResponse)) { 152 | throw new BluetoothAuthenticationException( 153 | "Device sent unexpected authentication response. " 154 | + "Not authorised: " + pinCodeURL); 155 | } 156 | logger.debug("Authentication succeeded. " 157 | + "Authentication response matches to the expected response: {}", pinCodeURL); 158 | } else { 159 | throw new BluetoothAuthenticationException( 160 | "Could not receive auth response. Timeout happened: " + pinCodeURL); 161 | } 162 | } else { 163 | logger.debug("Performing simple authentication. Sending pin code to device: {}", pinCodeChar.getURL()); 164 | if (!pinCodeChar.write(pinCode)) { 165 | throw new BluetoothAuthenticationException("Could not send pin code: " + pinCodeURL); 166 | } 167 | logger.debug("Authentication succeeded. Pin code has been sent: {}", pinCodeURL); 168 | } 169 | } 170 | 171 | private boolean readyForAuthentication(CharacteristicGovernor gov) { 172 | boolean ready = gov.isReady() && (!gov.isNotifiable() || gov.isNotifying()); 173 | logger.debug("Checking if characteristic is ready for authentication: {} : {}", gov.getURL(), ready); 174 | return ready; 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/AdapterGovernorImpl.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.sputnikdev.bluetooth.URL; 26 | import org.sputnikdev.bluetooth.manager.AdapterGovernor; 27 | import org.sputnikdev.bluetooth.manager.AdapterListener; 28 | import org.sputnikdev.bluetooth.manager.BluetoothObjectType; 29 | import org.sputnikdev.bluetooth.manager.BluetoothObjectVisitor; 30 | import org.sputnikdev.bluetooth.manager.DeviceGovernor; 31 | import org.sputnikdev.bluetooth.manager.NotReadyException; 32 | import org.sputnikdev.bluetooth.manager.transport.Adapter; 33 | import org.sputnikdev.bluetooth.manager.transport.Notification; 34 | 35 | import java.util.List; 36 | import java.util.concurrent.CopyOnWriteArrayList; 37 | import java.util.function.Function; 38 | 39 | /** 40 | * 41 | * @author Vlad Kolotov 42 | */ 43 | class AdapterGovernorImpl extends AbstractBluetoothObjectGovernor implements AdapterGovernor { 44 | 45 | private Logger logger = LoggerFactory.getLogger(AdapterGovernorImpl.class); 46 | 47 | private final List adapterListeners = new CopyOnWriteArrayList<>(); 48 | 49 | private PoweredNotification poweredNotification; 50 | private DiscoveringNotification discoveringNotification; 51 | 52 | private boolean poweredControl = true; 53 | private boolean discoveringControl = true; 54 | private double signalPropagationExponent; 55 | 56 | AdapterGovernorImpl(BluetoothManagerImpl bluetoothManager, URL url) { 57 | super(bluetoothManager, url); 58 | } 59 | 60 | void init(Adapter adapter) { 61 | logger.debug("Initializing adapter governor: {}", url); 62 | enablePoweredNotifications(adapter); 63 | enableDiscoveringNotifications(adapter); 64 | logger.trace("Adapter governor initialization performed: {}", url); 65 | } 66 | 67 | void update(Adapter adapter) { 68 | logger.debug("Updating adapter governor: {}", url); 69 | updatePowered(adapter); 70 | if (adapter.isPowered()) { 71 | updateDiscovering(adapter); 72 | } 73 | updateLastInteracted(); 74 | logger.trace("Adapter governor update performed: {}", url); 75 | } 76 | 77 | @Override 78 | void reset(Adapter adapter) { 79 | logger.debug("Resetting adapter governor: {}", url); 80 | try { 81 | adapter.disablePoweredNotifications(); 82 | adapter.disableDiscoveringNotifications(); 83 | // force stop discovery and ignore any error 84 | adapter.stopDiscovery(); 85 | } catch (Exception ex) { 86 | logger.warn("Error occurred while resetting adapter native object: {} : {}", url, ex.getMessage()); 87 | } 88 | poweredNotification = null; 89 | discoveringNotification = null; 90 | logger.trace("Adapter governor reset performed: {}", url); 91 | } 92 | 93 | @Override 94 | public void dispose() { 95 | super.dispose(); 96 | logger.debug("Disposing adapter governor: {}", url); 97 | adapterListeners.clear(); 98 | logger.trace("Adapter governor disposed: {}", url); 99 | } 100 | 101 | @Override 102 | public boolean isUpdatable() { 103 | return true; 104 | } 105 | 106 | @Override 107 | public boolean getPoweredControl() { 108 | return poweredControl; 109 | } 110 | 111 | @Override 112 | public void setPoweredControl(boolean poweredControl) { 113 | this.poweredControl = poweredControl; 114 | } 115 | 116 | @Override 117 | public boolean isPowered() throws NotReadyException { 118 | return isReady() && interact("isPowered", Adapter::isPowered); 119 | } 120 | 121 | @Override 122 | public boolean getDiscoveringControl() { 123 | return discoveringControl; 124 | } 125 | 126 | @Override 127 | public void setDiscoveringControl(boolean discovering) { 128 | discoveringControl = discovering; 129 | } 130 | 131 | @Override 132 | public boolean isDiscovering() throws NotReadyException { 133 | return isReady() && interact("isDiscovering", Adapter::isDiscovering); 134 | } 135 | 136 | @Override 137 | public void setAlias(String alias) throws NotReadyException { 138 | interact("setAlias", Adapter::setAlias, alias); 139 | } 140 | 141 | @Override 142 | public String getAlias() throws NotReadyException { 143 | return interact("getAlias", Adapter::getAlias); 144 | } 145 | 146 | @Override 147 | public String getName() throws NotReadyException { 148 | return interact("getName", Adapter::getName); 149 | } 150 | 151 | @Override 152 | public String getDisplayName() throws NotReadyException { 153 | String alias = getAlias(); 154 | return alias != null ? alias : getName(); 155 | } 156 | 157 | @Override 158 | public double getSignalPropagationExponent() { 159 | return signalPropagationExponent; 160 | } 161 | 162 | @Override 163 | public void setSignalPropagationExponent(double signalPropagationExponent) { 164 | this.signalPropagationExponent = signalPropagationExponent; 165 | } 166 | 167 | @Override 168 | public List getDevices() throws NotReadyException { 169 | return interact("getDevices", 170 | (Function>) adapter -> BluetoothManagerUtils.getURLs(adapter.getDevices())); 171 | } 172 | 173 | @Override 174 | public List getDeviceGovernors() throws NotReadyException { 175 | return interact("getDeviceGovernors", 176 | adapter -> (List) bluetoothManager.getGovernors(adapter.getDevices())); 177 | } 178 | 179 | @Override 180 | public String toString() { 181 | String result = "[Adapter] " + getURL(); 182 | if (isReady()) { 183 | String displayName = getDisplayName(); 184 | if (displayName != null) { 185 | result += " [" + displayName + "]"; 186 | } 187 | } 188 | return result; 189 | } 190 | 191 | @Override 192 | public BluetoothObjectType getType() { 193 | return BluetoothObjectType.ADAPTER; 194 | } 195 | 196 | @Override 197 | public void accept(BluetoothObjectVisitor visitor) throws Exception { 198 | visitor.visit(this); 199 | } 200 | 201 | @Override 202 | public void addAdapterListener(AdapterListener adapterListener) { 203 | adapterListeners.add(adapterListener); 204 | } 205 | 206 | @Override 207 | public void removeAdapterListener(AdapterListener adapterListener) { 208 | adapterListeners.remove(adapterListener); 209 | } 210 | 211 | void notifyPowered(boolean powered) { 212 | logger.debug("Notifying adapter governor listener (powered): {} : {} : {}", 213 | url, adapterListeners.size(), powered); 214 | BluetoothManagerUtils.forEachSilently(adapterListeners, 215 | listener -> listener.powered(powered), logger, 216 | "Execution error of a powered listener: " + powered); 217 | } 218 | 219 | void notifyDiscovering(boolean discovering) { 220 | logger.debug("Notifying adapter governor listener (discovering): {} : {} : {}", 221 | url, adapterListeners.size(), discovering); 222 | BluetoothManagerUtils.forEachSilently(adapterListeners, 223 | listener -> listener.discovering(discovering), logger, 224 | "Execution error of a discovering listener: " + discovering); 225 | } 226 | 227 | private void updatePowered(Adapter adapter) { 228 | logger.trace("Updating adapter governor powered state: {}", url); 229 | boolean powered = adapter.isPowered(); 230 | logger.trace("Powered state: {} : {} (control) / {} (state)", url, poweredControl, powered); 231 | if (poweredControl != powered) { 232 | logger.debug("Setting powered: {} : {}", url, poweredControl); 233 | adapter.setPowered(poweredControl); 234 | if (!adapter.isPowered()) { 235 | throw new NotReadyException("Could not power adapter"); 236 | } 237 | } 238 | } 239 | 240 | private void updateDiscovering(Adapter adapter) { 241 | logger.trace("Updating adapter governor discovering state: {}", url); 242 | boolean isDiscovering = adapter.isDiscovering(); 243 | logger.trace("Discovering state: {} : {} (control) / {} (state)", url, discoveringControl, isDiscovering); 244 | if (discoveringControl && !isDiscovering) { 245 | logger.debug("Starting discovery: {}", url); 246 | adapter.startDiscovery(); 247 | } else if (!discoveringControl && isDiscovering) { 248 | logger.debug("Stopping discovery: {}", url); 249 | adapter.stopDiscovery(); 250 | } 251 | } 252 | 253 | private void enablePoweredNotifications(Adapter adapter) { 254 | logger.debug("Enabling powered notifications: {} : {} ", url, poweredNotification == null); 255 | if (poweredNotification == null) { 256 | poweredNotification = new PoweredNotification(); 257 | adapter.enablePoweredNotifications(poweredNotification); 258 | logger.trace("Powered notifications enabled: {}", url); 259 | } 260 | } 261 | 262 | private void enableDiscoveringNotifications(Adapter adapter) { 263 | logger.debug("Enabling discovering notifications: {} : {}", url, discoveringNotification == null); 264 | if (discoveringNotification == null) { 265 | discoveringNotification = new DiscoveringNotification(); 266 | adapter.enableDiscoveringNotifications(discoveringNotification); 267 | logger.trace("Discovering notifications enabled: {}", url); 268 | } 269 | } 270 | 271 | private class PoweredNotification implements Notification { 272 | @Override 273 | public void notify(Boolean powered) { 274 | notifyPowered(powered); 275 | updateLastInteracted(); 276 | } 277 | } 278 | 279 | private class DiscoveringNotification implements Notification { 280 | @Override 281 | public void notify(Boolean discovering) { 282 | notifyDiscovering(discovering); 283 | updateLastInteracted(); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/BluetoothManagerUtils.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.slf4j.Logger; 24 | import org.sputnikdev.bluetooth.URL; 25 | import org.sputnikdev.bluetooth.manager.transport.BluetoothObject; 26 | 27 | import java.time.Instant; 28 | import java.util.ArrayList; 29 | import java.util.Collection; 30 | import java.util.Collections; 31 | import java.util.List; 32 | import java.util.function.BiConsumer; 33 | import java.util.function.Consumer; 34 | import java.util.regex.Pattern; 35 | 36 | /** 37 | * Utility class. 38 | * @author Vlad Kolotov 39 | */ 40 | final class BluetoothManagerUtils { 41 | 42 | private static final Pattern MAC_PATTERN = Pattern.compile("(\\w\\w[:-]){5}\\w\\w"); 43 | 44 | private BluetoothManagerUtils() { } 45 | 46 | static List getURLs(List objects) { 47 | List urls = new ArrayList<>(objects.size()); 48 | for (BluetoothObject object : objects) { 49 | urls.add(object.getURL()); 50 | } 51 | return Collections.unmodifiableList(urls); 52 | } 53 | 54 | static void forEachSilently(Collection listeners, Consumer consumer, 55 | Logger logger, String error) { 56 | forEachSilently(listeners, consumer, ex -> { 57 | logger.error(error, ex); 58 | }); 59 | } 60 | 61 | static void forEachSilently(Collection listeners, BiConsumer consumer, V value, 62 | Logger logger, String error) { 63 | forEachSilently(listeners, consumer, value, ex -> { 64 | logger.error(error, ex); 65 | }); 66 | } 67 | 68 | static void forEachSilently(Collection objects, Consumer func, Consumer errorHandler) { 69 | objects.forEach(deviceDiscoveryListener -> { 70 | try { 71 | func.accept(deviceDiscoveryListener); 72 | } catch (Exception ex) { 73 | errorHandler.accept(ex); 74 | } 75 | }); 76 | } 77 | 78 | static void forEachSilently(Collection objects, BiConsumer func, V value, 79 | Consumer errorHandler) { 80 | objects.forEach(deviceDiscoveryListener -> { 81 | try { 82 | func.accept(deviceDiscoveryListener, value); 83 | } catch (Exception ex) { 84 | errorHandler.accept(ex); 85 | } 86 | }); 87 | } 88 | 89 | static Instant max(Instant first, Instant second) { 90 | if (first == null && second == null) { 91 | return null; 92 | } 93 | if (first != null && second == null) { 94 | return first; 95 | } 96 | if (first == null) { 97 | return second; 98 | } 99 | return first.isAfter(second) ? first : second; 100 | } 101 | 102 | static boolean isMacAddress(String name) { 103 | return name != null && MAC_PATTERN.matcher(name).matches(); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/BluetoothObjectGovernor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | import org.sputnikdev.bluetooth.manager.BluetoothGovernor; 4 | 5 | /** 6 | * 7 | * @author Vlad Kolotov 8 | */ 9 | interface BluetoothObjectGovernor extends BluetoothGovernor { 10 | 11 | /** 12 | * Initializing the governor. 13 | */ 14 | void init(); 15 | 16 | /** 17 | * Updating the governor. 18 | */ 19 | void update(); 20 | 21 | /** 22 | * Objects may decide if they can be updated. 23 | * @return 24 | */ 25 | boolean isUpdatable(); 26 | 27 | /** 28 | * Resetting the governor to be reused later. 29 | */ 30 | void reset(); 31 | 32 | /** 33 | * Disposing the governor so that it cannot be reused. 34 | */ 35 | void dispose(); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/CharacteristicGovernorImpl.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.sputnikdev.bluetooth.URL; 26 | import org.sputnikdev.bluetooth.manager.BluetoothObjectType; 27 | import org.sputnikdev.bluetooth.manager.BluetoothObjectVisitor; 28 | import org.sputnikdev.bluetooth.manager.CharacteristicGovernor; 29 | import org.sputnikdev.bluetooth.manager.NotReadyException; 30 | import org.sputnikdev.bluetooth.manager.ValueListener; 31 | import org.sputnikdev.bluetooth.manager.transport.Characteristic; 32 | import org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType; 33 | import org.sputnikdev.bluetooth.manager.transport.Notification; 34 | 35 | import java.time.Instant; 36 | import java.util.List; 37 | import java.util.Set; 38 | import java.util.concurrent.CopyOnWriteArrayList; 39 | 40 | /** 41 | * 42 | * @author Vlad Kolotov 43 | */ 44 | class CharacteristicGovernorImpl extends AbstractBluetoothObjectGovernor 45 | implements CharacteristicGovernor { 46 | 47 | private Logger logger = LoggerFactory.getLogger(CharacteristicGovernorImpl.class); 48 | 49 | private boolean authenticated; 50 | 51 | private List valueListeners = new CopyOnWriteArrayList<>(); 52 | private ValueNotification valueNotification; 53 | private boolean canNotify; 54 | private boolean notifying; 55 | private Instant lastNotified; 56 | 57 | CharacteristicGovernorImpl(BluetoothManagerImpl bluetoothManager, URL url) { 58 | super(bluetoothManager, url); 59 | } 60 | 61 | @Override 62 | void init(Characteristic characteristic) { 63 | logger.debug("Initializing characteristic governor: {}", url); 64 | canNotify = canNotify(characteristic); 65 | if (canNotify) { 66 | notifying = characteristic.isNotifying(); 67 | } 68 | logger.trace("Characteristic governor initialization performed: {} : {}", url, canNotify); 69 | } 70 | 71 | @Override 72 | void update(Characteristic characteristic) { 73 | logger.trace("Updating characteristic governor: {}", url); 74 | authenticated = bluetoothManager.getDeviceGovernor(url.getDeviceURL()).isAuthenticated(); 75 | 76 | if (canNotify) { 77 | logger.trace("Updating characteristic governor notifications state: {} : {} / {} / {}", 78 | url, valueListeners.isEmpty(), notifying, valueNotification == null); 79 | if (!valueListeners.isEmpty() && (!notifying || valueNotification == null)) { 80 | enableNotification(characteristic); 81 | } else if (valueListeners.isEmpty() && notifying) { 82 | disableNotification(characteristic); 83 | } 84 | } 85 | } 86 | 87 | @Override 88 | void reset(Characteristic characteristic) { 89 | logger.debug("Resetting characteristic governor: {}", url); 90 | valueNotification = null; 91 | try { 92 | if (canNotify && characteristic.isNotifying()) { 93 | characteristic.disableValueNotifications(); 94 | } 95 | } catch (Exception ex) { 96 | logger.debug("Error occurred while resetting characteristic: {} : {} ", url, ex.getMessage()); 97 | } 98 | authenticated = false; 99 | } 100 | 101 | @Override 102 | public void dispose() { 103 | super.dispose(); 104 | logger.debug("Disposing characteristic governor: {}", url); 105 | valueListeners.clear(); 106 | logger.trace("Characteristic governor disposed: {}", url); 107 | } 108 | 109 | @Override 110 | public boolean isUpdatable() { 111 | return bluetoothManager.getDeviceGovernor(url.getDeviceURL()).isAuthenticated(); 112 | } 113 | 114 | @Override 115 | public void addValueListener(ValueListener valueListener) { 116 | valueListeners.add(valueListener); 117 | bluetoothManager.scheduleForceUpdate(this); 118 | } 119 | 120 | @Override 121 | public void removeValueListener(ValueListener valueListener) { 122 | valueListeners.remove(valueListener); 123 | } 124 | 125 | @Override 126 | public Set getFlags() throws NotReadyException { 127 | return interact("getFlags", Characteristic::getFlags); 128 | } 129 | 130 | @Override 131 | public boolean isNotifiable() throws NotReadyException { 132 | Set flgs = getFlags(); 133 | return flgs.contains(CharacteristicAccessType.NOTIFY) || flgs.contains(CharacteristicAccessType.INDICATE); 134 | } 135 | 136 | @Override 137 | public boolean isNotifying() throws NotReadyException { 138 | return isReady() && interact("isNotifying", Characteristic::isNotifying); 139 | } 140 | 141 | @Override 142 | public boolean isWritable() throws NotReadyException { 143 | Set flgs = getFlags(); 144 | return flgs.contains(CharacteristicAccessType.WRITE) 145 | || flgs.contains(CharacteristicAccessType.WRITE_WITHOUT_RESPONSE); 146 | } 147 | 148 | @Override 149 | public boolean isReadable() throws NotReadyException { 150 | return getFlags().contains(CharacteristicAccessType.READ); 151 | } 152 | 153 | @Override 154 | public byte[] read() throws NotReadyException { 155 | if (!isReadable()) { 156 | throw new IllegalStateException("Characteristic is not readable: {}" + url); 157 | } 158 | return interact("read", Characteristic::readValue, true); 159 | } 160 | 161 | @Override 162 | public boolean write(byte[] data) throws NotReadyException { 163 | if (!isWritable()) { 164 | throw new IllegalStateException("Characteristic is not writable: {}" + url); 165 | } 166 | return interact("write", characteristic -> characteristic.writeValue(data), true); 167 | } 168 | 169 | @Override 170 | public String toString() { 171 | return "[Characteristic] " + getURL(); 172 | } 173 | 174 | @Override 175 | public BluetoothObjectType getType() { 176 | return BluetoothObjectType.CHARACTERISTIC; 177 | } 178 | 179 | @Override 180 | public void accept(BluetoothObjectVisitor visitor) throws Exception { 181 | visitor.visit(this); 182 | } 183 | 184 | @Override 185 | public Instant getLastNotified() { 186 | return lastNotified; 187 | } 188 | 189 | @Override 190 | public boolean isAuthenticated() { 191 | return authenticated; 192 | } 193 | 194 | @Override 195 | void notifyLastChanged() { 196 | notifyLastChanged(BluetoothManagerUtils.max(getLastInteracted(), lastNotified)); 197 | } 198 | 199 | private void updateLastNotified() { 200 | lastNotified = Instant.now(); 201 | } 202 | 203 | private void enableNotification(Characteristic characteristic) { 204 | logger.debug("Enabling characteristic notifications: {} : {} / {}", 205 | getURL(), valueNotification == null, canNotify); 206 | if (valueNotification == null && canNotify) { 207 | ValueNotification notification = new ValueNotification(); 208 | characteristic.enableValueNotifications(notification); 209 | valueNotification = notification; 210 | notifying = true; 211 | } 212 | } 213 | 214 | private void disableNotification(Characteristic characteristic) { 215 | logger.debug("Disabling characteristic notifications: {} : {} / {}", 216 | getURL(), valueNotification == null, canNotify); 217 | ValueNotification notification = valueNotification; 218 | valueNotification = null; 219 | if (notification != null && canNotify) { 220 | characteristic.disableValueNotifications(); 221 | notifying = false; 222 | } 223 | } 224 | 225 | private static boolean canNotify(Characteristic characteristic) { 226 | Set flgs = characteristic.getFlags(); 227 | return flgs.contains(CharacteristicAccessType.NOTIFY) || flgs.contains(CharacteristicAccessType.INDICATE); 228 | } 229 | 230 | private class ValueNotification implements Notification { 231 | @Override 232 | public void notify(byte[] data) { 233 | logger.trace("Characteristic value changed (notification): {}", url); 234 | updateLastInteracted(); 235 | updateLastNotified(); 236 | BluetoothManagerUtils.forEachSilently(valueListeners, ValueListener::changed, data, logger, 237 | "Execution error of a characteristic listener"); 238 | } 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/CombinedAdapterGovernorImpl.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.sputnikdev.bluetooth.URL; 26 | import org.sputnikdev.bluetooth.manager.AdapterDiscoveryListener; 27 | import org.sputnikdev.bluetooth.manager.AdapterGovernor; 28 | import org.sputnikdev.bluetooth.manager.AdapterListener; 29 | import org.sputnikdev.bluetooth.manager.BluetoothGovernor; 30 | import org.sputnikdev.bluetooth.manager.BluetoothObjectType; 31 | import org.sputnikdev.bluetooth.manager.BluetoothObjectVisitor; 32 | import org.sputnikdev.bluetooth.manager.CombinedGovernor; 33 | import org.sputnikdev.bluetooth.manager.DeviceGovernor; 34 | import org.sputnikdev.bluetooth.manager.DiscoveredAdapter; 35 | import org.sputnikdev.bluetooth.manager.GovernorListener; 36 | import org.sputnikdev.bluetooth.manager.NotReadyException; 37 | 38 | import java.time.Instant; 39 | import java.util.List; 40 | import java.util.Map; 41 | import java.util.concurrent.CompletableFuture; 42 | import java.util.concurrent.ConcurrentHashMap; 43 | import java.util.concurrent.CopyOnWriteArrayList; 44 | import java.util.concurrent.atomic.AtomicInteger; 45 | import java.util.function.Function; 46 | import java.util.function.Predicate; 47 | 48 | class CombinedAdapterGovernorImpl implements AdapterGovernor, CombinedGovernor, 49 | BluetoothObjectGovernor, AdapterDiscoveryListener { 50 | 51 | private Logger logger = LoggerFactory.getLogger(CombinedAdapterGovernorImpl.class); 52 | 53 | private final Map governors = new ConcurrentHashMap<>(); 54 | private final CompletableFutureService futureService = new CompletableFutureService<>(); 55 | private final BluetoothManagerImpl bluetoothManager; 56 | private final URL url; 57 | 58 | private final List governorListeners = new CopyOnWriteArrayList<>(); 59 | private final List adapterListeners = new CopyOnWriteArrayList<>(); 60 | 61 | private Instant lastInteracted; 62 | 63 | private boolean poweredControl = true; 64 | private boolean discoveringControl = true; 65 | private double signalPropagationExponent; 66 | 67 | private final ConcurrentBitMap ready = new ConcurrentBitMap(); 68 | private final ConcurrentBitMap powered = new ConcurrentBitMap(); 69 | private final ConcurrentBitMap discovering = new ConcurrentBitMap(); 70 | 71 | private final AtomicInteger governorsCount = new AtomicInteger(); 72 | 73 | CombinedAdapterGovernorImpl(BluetoothManagerImpl bluetoothManager, URL url) { 74 | this.bluetoothManager = bluetoothManager; 75 | this.url = url; 76 | } 77 | 78 | @Override 79 | public String getName() throws NotReadyException { 80 | return "Combined Bluetooth Adapter"; 81 | } 82 | 83 | @Override 84 | public String getAlias() throws NotReadyException { 85 | return null; 86 | } 87 | 88 | @Override 89 | public void setAlias(String alias) throws NotReadyException { 90 | 91 | } 92 | 93 | @Override 94 | public String getDisplayName() throws NotReadyException { 95 | return getName(); 96 | } 97 | 98 | @Override 99 | public boolean isPowered() throws NotReadyException { 100 | return powered.get(); 101 | } 102 | 103 | @Override 104 | public boolean getPoweredControl() { 105 | return poweredControl; 106 | } 107 | 108 | @Override 109 | public void setPoweredControl(boolean powered) { 110 | poweredControl = powered; 111 | governors.values().forEach( 112 | adapterGovernorHandler -> adapterGovernorHandler.adapterGovernor.setPoweredControl(powered)); 113 | } 114 | 115 | @Override 116 | public boolean isDiscovering() throws NotReadyException { 117 | return discovering.get(); 118 | } 119 | 120 | @Override 121 | public boolean getDiscoveringControl() { 122 | return discoveringControl; 123 | } 124 | 125 | @Override 126 | public void setDiscoveringControl(boolean discovering) { 127 | discoveringControl = discovering; 128 | governors.values().forEach( 129 | adapterGovernorHandler -> adapterGovernorHandler.adapterGovernor.setDiscoveringControl(discovering)); 130 | } 131 | 132 | @Override 133 | public double getSignalPropagationExponent() { 134 | return signalPropagationExponent; 135 | } 136 | 137 | @Override 138 | public void setSignalPropagationExponent(double exponent) { 139 | signalPropagationExponent = exponent; 140 | governors.values().forEach(adapterGovernorHandler -> adapterGovernorHandler.adapterGovernor 141 | .setSignalPropagationExponent(exponent)); 142 | } 143 | 144 | @Override 145 | public List getDevices() throws NotReadyException { 146 | return null; 147 | } 148 | 149 | @Override 150 | public List getDeviceGovernors() throws NotReadyException { 151 | return null; 152 | } 153 | 154 | @Override 155 | public void init() { 156 | bluetoothManager.addAdapterDiscoveryListener(this); 157 | bluetoothManager.getRegisteredGovernors().forEach(this::registerGovernor); 158 | bluetoothManager.getDiscoveredAdapters().stream().map(DiscoveredAdapter::getURL) 159 | .forEach(this::registerGovernor); 160 | } 161 | 162 | @Override 163 | public void update() { 164 | futureService.completeSilently(this); 165 | } 166 | 167 | @Override 168 | public boolean isUpdatable() { 169 | return true; 170 | } 171 | 172 | @Override 173 | public void reset() { /* do nothing */ } 174 | 175 | @Override 176 | public void dispose() { 177 | bluetoothManager.removeAdapterDiscoveryListener(this); 178 | governors.clear(); 179 | governorListeners.clear(); 180 | adapterListeners.clear(); 181 | futureService.clear(); 182 | } 183 | 184 | @Override 185 | public URL getURL() { 186 | return url; 187 | } 188 | 189 | @Override 190 | public boolean isReady() { 191 | return ready.get(); 192 | } 193 | 194 | @Override 195 | public BluetoothObjectType getType() { 196 | return BluetoothObjectType.ADAPTER; 197 | } 198 | 199 | @Override 200 | public Instant getLastInteracted() { 201 | return lastInteracted; 202 | } 203 | 204 | @Override 205 | public void accept(BluetoothObjectVisitor visitor) throws Exception { 206 | visitor.visit(this); 207 | } 208 | 209 | @Override 210 | public void addAdapterListener(AdapterListener adapterListener) { 211 | adapterListeners.add(adapterListener); 212 | } 213 | 214 | @Override 215 | public void removeAdapterListener(AdapterListener adapterListener) { 216 | adapterListeners.remove(adapterListener); 217 | } 218 | 219 | @Override 220 | public void addGovernorListener(GovernorListener listener) { 221 | governorListeners.add(listener); 222 | } 223 | 224 | @Override 225 | public void removeGovernorListener(GovernorListener listener) { 226 | governorListeners.remove(listener); 227 | } 228 | 229 | @Override 230 | public void discovered(DiscoveredAdapter adapter) { 231 | registerGovernor(adapter.getURL()); 232 | } 233 | 234 | @Override 235 | public void adapterLost(URL address) { /* do nothing */ } 236 | 237 | @Override 238 | @SuppressWarnings({"unchecked", "rawtypes"}) 239 | public CompletableFuture when(Predicate predicate, Function function) { 240 | return futureService.submit(this, (Predicate) predicate, 241 | (Function) function); 242 | } 243 | 244 | private void registerGovernor(URL url) { 245 | if (governorsCount.get() > 63) { 246 | throw new IllegalStateException("Shared Device Governor can only span upto 63 device governors."); 247 | } 248 | if (url.isAdapter() && !url.equals(this.url)) { 249 | governors.computeIfAbsent(url, newUrl -> { 250 | AdapterGovernor deviceGovernor = bluetoothManager.getAdapterGovernor(url); 251 | return new AdapterGovernorHandler(deviceGovernor, governorsCount.getAndIncrement()); 252 | }); 253 | } 254 | } 255 | 256 | private void updateLastInteracted(Instant lastActivity) { 257 | if (lastInteracted == null || lastInteracted.isBefore(lastActivity)) { 258 | lastInteracted = lastActivity; 259 | BluetoothManagerUtils.forEachSilently(governorListeners, listener -> { 260 | listener.lastUpdatedChanged(lastActivity); 261 | }, logger, "Execution error of a governor listener: last changed"); 262 | } 263 | } 264 | 265 | private final class AdapterGovernorHandler implements GovernorListener, AdapterListener { 266 | 267 | private final AdapterGovernor adapterGovernor; 268 | private final int index; 269 | 270 | private AdapterGovernorHandler(AdapterGovernor adapterGovernor, int index) { 271 | this.adapterGovernor = adapterGovernor; 272 | this.index = index; 273 | this.adapterGovernor.addAdapterListener(this); 274 | this.adapterGovernor.addGovernorListener(this); 275 | this.adapterGovernor.setPoweredControl(poweredControl); 276 | this.adapterGovernor.setDiscoveringControl(discoveringControl); 277 | this.adapterGovernor.setSignalPropagationExponent(signalPropagationExponent); 278 | ready(true); 279 | } 280 | 281 | @Override 282 | public void powered(boolean newState) { 283 | powered.cumulativeSet(index, newState, () -> { 284 | BluetoothManagerUtils.forEachSilently(adapterListeners, AdapterListener::powered, newState, 285 | logger, "Execution error of a Powered listener"); 286 | }); 287 | } 288 | 289 | @Override 290 | public void discovering(boolean newState) { 291 | discovering.cumulativeSet(index, newState, () -> { 292 | BluetoothManagerUtils.forEachSilently(adapterListeners, AdapterListener::discovering, newState, 293 | logger, "Execution error of a Discovering listener"); 294 | }); 295 | } 296 | 297 | @Override 298 | public void ready(boolean newState) { 299 | ready.cumulativeSet(index, newState, () -> { 300 | BluetoothManagerUtils.forEachSilently(governorListeners, GovernorListener::ready, newState, 301 | logger, "Execution error of a governor listener: ready"); 302 | }); 303 | } 304 | 305 | @Override 306 | public void lastUpdatedChanged(Instant lastActivity) { 307 | updateLastInteracted(lastActivity); 308 | } 309 | 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/CombinedCharacteristicGovernorImpl.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.sputnikdev.bluetooth.URL; 26 | import org.sputnikdev.bluetooth.manager.AdapterDiscoveryListener; 27 | import org.sputnikdev.bluetooth.manager.BluetoothGovernor; 28 | import org.sputnikdev.bluetooth.manager.BluetoothObjectType; 29 | import org.sputnikdev.bluetooth.manager.BluetoothObjectVisitor; 30 | import org.sputnikdev.bluetooth.manager.CharacteristicGovernor; 31 | import org.sputnikdev.bluetooth.manager.CombinedGovernor; 32 | import org.sputnikdev.bluetooth.manager.DiscoveredAdapter; 33 | import org.sputnikdev.bluetooth.manager.GovernorListener; 34 | import org.sputnikdev.bluetooth.manager.ManagerListener; 35 | import org.sputnikdev.bluetooth.manager.NotReadyException; 36 | import org.sputnikdev.bluetooth.manager.ValueListener; 37 | import org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType; 38 | 39 | import java.time.Instant; 40 | import java.util.HashSet; 41 | import java.util.List; 42 | import java.util.Set; 43 | import java.util.concurrent.CompletableFuture; 44 | import java.util.concurrent.CopyOnWriteArrayList; 45 | import java.util.function.Function; 46 | import java.util.function.Predicate; 47 | 48 | /** 49 | * 50 | * @author Vlad Kolotov 51 | */ 52 | class CombinedCharacteristicGovernorImpl 53 | implements CharacteristicGovernor, BluetoothObjectGovernor, CombinedGovernor { 54 | 55 | private Logger logger = LoggerFactory.getLogger(CombinedCharacteristicGovernorImpl.class); 56 | 57 | private BluetoothManagerImpl bluetoothManager; 58 | private final URL url; 59 | 60 | private final ManagerListener delegateListener = new DelegatesListener(); 61 | 62 | private CharacteristicGovernor delegate; 63 | private final List valueListeners = new CopyOnWriteArrayList<>(); 64 | private final List governorListeners = new CopyOnWriteArrayList<>(); 65 | private final CompletableFutureService futureService = new CompletableFutureService<>(); 66 | private Instant lastInteracted; 67 | private Instant lastNotified; 68 | 69 | 70 | CombinedCharacteristicGovernorImpl(BluetoothManagerImpl bluetoothManager, URL url) { 71 | this.bluetoothManager = bluetoothManager; 72 | this.url = url; 73 | } 74 | 75 | @Override 76 | public Set getFlags() throws NotReadyException { 77 | return getDelegate().getFlags(); 78 | } 79 | 80 | @Override 81 | public boolean isReadable() throws NotReadyException { 82 | return getDelegate().isReadable(); 83 | } 84 | 85 | @Override 86 | public boolean isWritable() throws NotReadyException { 87 | return getDelegate().isWritable(); 88 | } 89 | 90 | @Override 91 | public boolean isNotifiable() throws NotReadyException { 92 | return getDelegate().isNotifiable(); 93 | } 94 | 95 | @Override 96 | public boolean isNotifying() throws NotReadyException { 97 | return getDelegate().isNotifying(); 98 | } 99 | 100 | @Override 101 | public byte[] read() throws NotReadyException { 102 | return getDelegate().read(); 103 | } 104 | 105 | @Override 106 | public boolean write(byte[] data) throws NotReadyException { 107 | return getDelegate().write(data); 108 | } 109 | 110 | @Override 111 | public void addValueListener(ValueListener valueListener) { 112 | synchronized (delegateListener) { 113 | valueListeners.add(valueListener); 114 | if (delegate != null) { 115 | delegate.addValueListener(valueListener); 116 | } 117 | } 118 | } 119 | 120 | @Override 121 | public void removeValueListener(ValueListener valueListener) { 122 | synchronized (delegateListener) { 123 | valueListeners.remove(valueListener); 124 | if (delegate != null) { 125 | delegate.removeValueListener(valueListener); 126 | } 127 | } 128 | } 129 | 130 | @Override 131 | public URL getURL() { 132 | return url; 133 | } 134 | 135 | @Override 136 | public boolean isReady() { 137 | return delegate != null && delegate.isReady(); 138 | } 139 | 140 | @Override 141 | public BluetoothObjectType getType() { 142 | return BluetoothObjectType.CHARACTERISTIC; 143 | } 144 | 145 | @Override 146 | public Instant getLastInteracted() { 147 | return delegate != null ? delegate.getLastInteracted() : lastInteracted; 148 | } 149 | 150 | @Override 151 | public Instant getLastNotified() { 152 | return delegate != null ? delegate.getLastNotified() : lastNotified; 153 | } 154 | 155 | @Override 156 | public void accept(BluetoothObjectVisitor visitor) throws Exception { 157 | if (delegate != null) { 158 | visitor.visit(delegate); 159 | } 160 | } 161 | 162 | @Override 163 | public void addGovernorListener(GovernorListener listener) { 164 | synchronized (delegateListener) { 165 | governorListeners.add(listener); 166 | if (delegate != null) { 167 | delegate.addGovernorListener(listener); 168 | } 169 | } 170 | } 171 | 172 | @Override 173 | public void removeGovernorListener(GovernorListener listener) { 174 | synchronized (delegateListener) { 175 | governorListeners.remove(listener); 176 | if (delegate != null) { 177 | delegate.removeGovernorListener(listener); 178 | } 179 | } 180 | } 181 | 182 | @Override 183 | public void init() { 184 | logger.debug("Initializing combined characteristic governor: {}", url); 185 | bluetoothManager.addManagerListener(delegateListener); 186 | update(); 187 | logger.debug("Combined characteristic governor initialization completed: {}", url); 188 | } 189 | 190 | @Override 191 | public void update() { 192 | logger.debug("Updating combined characteristic governor: {}", url); 193 | if (delegate == null) { 194 | bluetoothManager.getDiscoveredAdapters().stream() 195 | .filter(CombinedCharacteristicGovernorImpl::notCombined) 196 | .map(this::getDelegate) 197 | .filter(BluetoothGovernor::isReady) 198 | .findFirst() 199 | .ifPresent(this::installDelegate); 200 | } 201 | futureService.completeSilently(this); 202 | logger.debug("Combined characteristic governor update completed: {}", url); 203 | } 204 | 205 | @Override 206 | public boolean isUpdatable() { 207 | return true; 208 | } 209 | 210 | @Override 211 | public void reset() { 212 | if (delegate != null) { 213 | uninstallDelegate(delegate.getURL()); 214 | } 215 | } 216 | 217 | @Override 218 | public void dispose() { 219 | bluetoothManager.removeManagerListener(delegateListener); 220 | reset(); 221 | governorListeners.clear(); 222 | valueListeners.clear(); 223 | futureService.clear(); 224 | } 225 | 226 | @Override 227 | @SuppressWarnings({"unchecked", "rawtypes"}) 228 | public CompletableFuture when(Predicate predicate, Function function) { 229 | return futureService.submit(this, (Predicate) predicate, 230 | (Function) function); 231 | } 232 | 233 | @Override 234 | public boolean isAuthenticated() { 235 | return delegate != null && delegate.isAuthenticated(); 236 | } 237 | 238 | private void installDelegate(CharacteristicGovernor delegate) { 239 | synchronized (delegateListener) { 240 | if (this.delegate == null) { 241 | logger.debug("Installing delegate: {}", delegate.getURL()); 242 | this.delegate = delegate; 243 | governorListeners.forEach(delegate::addGovernorListener); 244 | valueListeners.forEach(delegate::addValueListener); 245 | lastInteracted = delegate.getLastInteracted(); 246 | lastNotified = delegate.getLastNotified(); 247 | } else if (!this.delegate.equals(delegate)) { 248 | throw new IllegalStateException("Delegate un-ready event has been missed: " + url); 249 | } else { 250 | logger.debug("Skipping delegate as it has been installed already: " + url); 251 | } 252 | } 253 | bluetoothManager.notify(() -> { 254 | if (delegate.isReady()) { 255 | BluetoothManagerUtils.forEachSilently(governorListeners, GovernorListener::ready, true, logger, 256 | "Execution error of a governor listener: ready"); 257 | } 258 | BluetoothManagerUtils.forEachSilently(governorListeners, GovernorListener::lastUpdatedChanged, 259 | lastInteracted, logger,"Execution error of a governor listener: lastUpdatedChanged"); 260 | }); 261 | } 262 | 263 | private void uninstallDelegate(URL delegateURL) { 264 | CharacteristicGovernor delegate = this.delegate; 265 | if (delegate != null && delegate.getURL().equals(delegateURL)) { 266 | synchronized (delegateListener) { 267 | governorListeners.forEach(delegate::removeGovernorListener); 268 | valueListeners.forEach(delegate::removeValueListener); 269 | lastInteracted = delegate.getLastInteracted(); 270 | lastNotified = delegate.getLastNotified(); 271 | this.delegate = null; 272 | } 273 | } 274 | } 275 | 276 | private CharacteristicGovernor getDelegate() { 277 | CharacteristicGovernor delegate = this.delegate; 278 | if (delegate != null) { 279 | return delegate; 280 | } 281 | throw new NotReadyException("Combined characteristic governor is not ready yet"); 282 | } 283 | 284 | private class DelegatesListener implements ManagerListener { 285 | @Override 286 | public void ready(BluetoothGovernor governor, boolean isReady) { 287 | if (governor instanceof CharacteristicGovernor 288 | && governor.getURL().copyWithProtocol(null).copyWithAdapter(COMBINED_ADDRESS).equals(url)) { 289 | if (isReady) { 290 | installDelegate((CharacteristicGovernor) governor); 291 | } else { 292 | uninstallDelegate(governor.getURL()); 293 | } 294 | } 295 | } 296 | } 297 | 298 | private static boolean notCombined(DiscoveredAdapter adapter) { 299 | return !COMBINED_ADDRESS.equals(adapter.getURL().getAdapterAddress()); 300 | } 301 | 302 | private CharacteristicGovernor getDelegate(DiscoveredAdapter adapter) { 303 | return bluetoothManager.getCharacteristicGovernor(url.copyWithAdapter(adapter.getURL().getAdapterAddress())); 304 | } 305 | 306 | } 307 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/CompletableFutureService.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.sputnikdev.bluetooth.manager.BluetoothGovernor; 6 | import org.sputnikdev.bluetooth.manager.BluetoothInteractionException; 7 | import org.sputnikdev.bluetooth.manager.NotReadyException; 8 | import org.sputnikdev.bluetooth.manager.auth.BluetoothAuthenticationException; 9 | 10 | import java.util.Iterator; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.ConcurrentLinkedQueue; 13 | import java.util.function.Function; 14 | import java.util.function.Predicate; 15 | 16 | class CompletableFutureService { 17 | 18 | private Logger logger = LoggerFactory.getLogger(CompletableFutureService.class); 19 | 20 | private final ConcurrentLinkedQueue> futures = 21 | new ConcurrentLinkedQueue<>(); 22 | 23 | CompletableFuture submit(G governor, Predicate predicate, Function function) { 24 | DeferredCompletableFuture future = new DeferredCompletableFuture<>(predicate, function); 25 | 26 | try { 27 | if (!predicate.test(governor)) { 28 | logger.debug("Future is not ready to be completed immediately: {} : {}", governor.getURL(), predicate); 29 | futures.add(future); 30 | return future; 31 | } 32 | 33 | logger.debug("Trying to complete future immediately: {} : {}", governor.getURL(), predicate); 34 | 35 | future.complete(function.apply(governor)); 36 | } catch (BluetoothInteractionException | NotReadyException | BluetoothAuthenticationException nativeException) { 37 | logger.warn("Bluetooth error happened while completing a future immediately: {} : {}", 38 | governor.getURL(), nativeException.getMessage()); 39 | futures.add(future); 40 | } catch (Exception ex) { 41 | logger.error("Application error happened while completing a ready future: {}", governor.getURL(), ex); 42 | future.completeExceptionally(ex); 43 | } 44 | 45 | return future; 46 | } 47 | 48 | void completeSilently(G governor) { 49 | try { 50 | complete(governor); 51 | } catch (Exception ex) { 52 | logger.warn("Error occurred while completing (silently) futures: {}", ex.getMessage()); 53 | } 54 | } 55 | 56 | @SuppressWarnings({"rawtypes", "unchecked"}) 57 | void complete(G governor) { 58 | logger.trace("Trying to complete futures: {} : {}", governor.getURL(), futures.size()); 59 | 60 | for (Iterator> iterator = futures.iterator(); iterator.hasNext(); ) { 61 | DeferredCompletableFuture next = iterator.next(); 62 | if (next.isCancelled() || next.isDone()) { 63 | iterator.remove(); 64 | continue; 65 | } 66 | 67 | try { 68 | if (!next.getPredicate().test(governor)) { 69 | continue; 70 | } 71 | next.complete(next.getFunction().apply(governor)); 72 | } catch (BluetoothInteractionException | NotReadyException nativeException) { 73 | logger.warn("Bluetooth error happened while competing a future: {} : {}", 74 | governor.getURL(), nativeException.getMessage()); 75 | // put affecting future to the end 76 | iterator.remove(); 77 | futures.add(next); 78 | throw nativeException; 79 | } catch (Exception ex) { 80 | logger.warn("Application error happened while competing a future: {} : {}", 81 | governor.getURL(), ex.getMessage()); 82 | next.completeExceptionally(ex); 83 | } 84 | iterator.remove(); 85 | } 86 | } 87 | 88 | void clear() { 89 | futures.forEach(future -> future.cancel(true)); 90 | futures.clear(); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/ConcurrentBitMap.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * org.sputnikdev:bluetooth-manager 4 | * %% 5 | * Copyright (C) 2017 Sputnik Dev 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | 21 | package org.sputnikdev.bluetooth.manager.impl; 22 | 23 | import java.util.Queue; 24 | import java.util.concurrent.ConcurrentLinkedQueue; 25 | import java.util.concurrent.atomic.AtomicLong; 26 | 27 | /** 28 | * A utility class that can accommodate 63 boolean flags. It is similar to {@link java.util.BitSet} 29 | * but synchronized and provides some "atomic" utility methods for tracking changes. 30 | * @author Vlad Kolotov 31 | */ 32 | class ConcurrentBitMap { 33 | 34 | private final AtomicLong bits = new AtomicLong(); 35 | private final Queue notifications = new ConcurrentLinkedQueue<>(); 36 | 37 | /** 38 | * Sets a new cumulative state for the bitmap field. 39 | * @param index index of the new state 40 | * @param newState value of the new state 41 | */ 42 | void cumulativeSet(int index, boolean newState) { 43 | cumulativeSet(index, newState, null, null); 44 | } 45 | 46 | /** 47 | * Sets a new cumulative state for the bitmap field. 48 | * @param index index of the new state 49 | * @param newState value of the new state 50 | * @param changed triggered if the overall state changes 51 | */ 52 | void cumulativeSet(int index, boolean newState, Runnable changed) { 53 | cumulativeSet(index, newState, changed, null); 54 | } 55 | 56 | /** 57 | * Sets a new exclusive state for the bitmap field. 58 | * @param index index of the new state 59 | * @param newState value of the new state 60 | */ 61 | void exclusiveSet(int index, boolean newState) { 62 | exclusiveSet(index, newState, null, null); 63 | } 64 | 65 | /** 66 | * Sets a new exclusive state for the bitmap field. 67 | * @param index index of the new state 68 | * @param newState value of the new state 69 | * @param changed triggered if the overall state changes 70 | */ 71 | void exclusiveSet(int index, boolean newState, Runnable changed) { 72 | exclusiveSet(index, newState, changed, null); 73 | } 74 | 75 | /** 76 | * Sets a new cumulative state for the bitmap field. 77 | * @param index index of the new state 78 | * @param newState value of the new state 79 | * @param changed triggered if the overall state changes 80 | * @param notChanged triggered if the overall state does not change 81 | */ 82 | void cumulativeSet(int index, boolean newState, Runnable changed, Runnable notChanged) { 83 | if (index < 0 || index > 63) { 84 | throw new IllegalStateException("Invalid index, must be between 0 and 63: " + index); 85 | } 86 | bits.getAndUpdate(current -> { 87 | long updated = newState ? current | (1L << index) : current & ~(1L << index); 88 | if (updated > 0 && current == 0 || updated == 0 && current > 0) { 89 | if (changed != null) { 90 | notifications.add(changed); 91 | } 92 | } else if (notChanged != null) { 93 | notifications.add(notChanged); 94 | } 95 | return updated; 96 | }); 97 | handleNotifications(); 98 | } 99 | 100 | /** 101 | * Sets a new exclusive state for the bitmap field. 102 | * @param index index of the new state 103 | * @param newState value of the new state 104 | * @param changed triggered if the overall state changes 105 | * @param notChanged triggered if the overall state does not change 106 | */ 107 | void exclusiveSet(int index, boolean newState, Runnable changed, Runnable notChanged) { 108 | if (index < 0 || index > 63) { 109 | throw new IllegalStateException("Invalid index, must be between 0 and 63: " + index); 110 | } 111 | bits.getAndUpdate(current -> { 112 | long updated = newState ? (1L << index) : current & ~(1L << index); 113 | if (updated > 0 && current == 0 || updated == 0 && current > 0) { 114 | if (changed != null) { 115 | notifications.add(changed); 116 | } 117 | } else if (notChanged != null) { 118 | notifications.add(notChanged); 119 | } 120 | return updated; 121 | }); 122 | handleNotifications(); 123 | } 124 | 125 | /** 126 | * Returns cumulative value (if any of bits is set to 1). 127 | * @return true if any of bits is set to 1, false otherwise 128 | */ 129 | boolean get() { 130 | return bits.get() > 0; 131 | } 132 | 133 | /** 134 | * Returns the one bit index. If multiple one bits found, an IllegalStateException is thrown. 135 | * If no one bits found, -1 returned. 136 | * @return one bit index 137 | */ 138 | int getUniqueIndex() { 139 | long state = bits.get(); 140 | if (Long.bitCount(state) > 1) { 141 | throw new IllegalStateException("Multiple one bits found"); 142 | } 143 | return Long.numberOfTrailingZeros(state); 144 | } 145 | 146 | void reset() { 147 | bits.set(0); 148 | } 149 | 150 | private void handleNotifications() { 151 | notifications.removeIf(notification -> { 152 | notification.run(); 153 | return true; 154 | }); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/impl/DeferredCompletableFuture.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.function.Function; 5 | import java.util.function.Predicate; 6 | 7 | class DeferredCompletableFuture extends CompletableFuture { 8 | 9 | private final Predicate predicate; 10 | private final Function function; 11 | 12 | DeferredCompletableFuture(Predicate predicate, Function function) { 13 | this.predicate = predicate; 14 | this.function = function; 15 | } 16 | 17 | public Predicate getPredicate() { 18 | return predicate; 19 | } 20 | 21 | Function getFunction() { 22 | return function; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/Adapter.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.util.List; 24 | 25 | /** 26 | * 27 | * @author Vlad Kolotov 28 | */ 29 | public interface Adapter extends BluetoothObject { 30 | 31 | String getName(); 32 | 33 | String getAlias(); 34 | void setAlias(String s); 35 | 36 | boolean isDiscovering(); 37 | void enableDiscoveringNotifications(Notification notification); 38 | void disableDiscoveringNotifications(); 39 | boolean startDiscovery(); 40 | boolean stopDiscovery(); 41 | 42 | boolean isPowered(); 43 | void setPowered(boolean b); 44 | void enablePoweredNotifications(Notification notification); 45 | void disablePoweredNotifications(); 46 | 47 | List getDevices(); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/BluetoothObject.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | 25 | /** 26 | * 27 | * @author Vlad Kolotov 28 | */ 29 | public interface BluetoothObject { 30 | 31 | URL getURL(); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/BluetoothObjectFactory.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.URL; 24 | import org.sputnikdev.bluetooth.manager.DiscoveredAdapter; 25 | import org.sputnikdev.bluetooth.manager.DiscoveredDevice; 26 | 27 | import java.util.Map; 28 | import java.util.Set; 29 | 30 | /** 31 | * A root interface for all Bluetooth transport implementations. 32 | * 33 | * @author Vlad Kolotov 34 | */ 35 | public interface BluetoothObjectFactory { 36 | 37 | 38 | /** 39 | * Returns an adapter by its URl. URL may not contain 'protocol' part. 40 | * @param url adapter URL 41 | * @return an adapter 42 | */ 43 | Adapter getAdapter(URL url); 44 | 45 | /** 46 | * Returns a device by its URl. URL may not contain 'protocol' part. 47 | * @param url device URL 48 | * @return a device 49 | */ 50 | Device getDevice(URL url); 51 | 52 | /** 53 | * Returns a characteristic by its URl. URL may not contain 'protocol' part. 54 | * @param url characteristic URL 55 | * @return a characteristic 56 | */ 57 | Characteristic getCharacteristic(URL url); 58 | 59 | /** 60 | * Returns all discovered adapters by all registered transports. 61 | * @return all discovered adapters 62 | */ 63 | Set getDiscoveredAdapters(); 64 | 65 | /** 66 | * Returns all discovered devices by all registered transports. 67 | * @return all discovered devices 68 | */ 69 | Set getDiscoveredDevices(); 70 | 71 | /** 72 | * Returns transport protocol name. 73 | * @return transport protocol name 74 | */ 75 | String getProtocolName(); 76 | 77 | /** 78 | * The bluetooth manager might call this method in order to pass some configuration variables. 79 | * @param config configuration 80 | */ 81 | void configure(Map config); 82 | 83 | /** 84 | * Disposes and removes registered object from the transport. 85 | * @param url device to remove 86 | */ 87 | void dispose(URL url); 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/Characteristic.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.util.Set; 24 | 25 | /** 26 | * 27 | * @author Vlad Kolotov 28 | */ 29 | public interface Characteristic extends BluetoothObject { 30 | 31 | Set getFlags(); 32 | 33 | boolean isNotifying(); 34 | 35 | void disableValueNotifications(); 36 | 37 | byte[] readValue(); 38 | 39 | boolean writeValue(byte[] data); 40 | 41 | void enableValueNotifications(Notification notification); 42 | 43 | boolean isNotificationConfigurable(); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/CharacteristicAccessType.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | import java.util.Set; 4 | import java.util.stream.Collectors; 5 | import java.util.stream.Stream; 6 | 7 | /** 8 | * Characteristic properties (access type). 9 | * Read the spec here: https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.attribute.gatt.characteristic_declaration.xml 10 | */ 11 | public enum CharacteristicAccessType { 12 | 13 | BROADCAST(0x01), 14 | READ(0x02), 15 | WRITE_WITHOUT_RESPONSE(0x04), 16 | WRITE(0x08), 17 | NOTIFY(0x10), 18 | INDICATE(0x20), 19 | AUTHENTICATED_SIGNED_WRITES(0x40), 20 | EXTENDED_PROPERTIES(0x80); 21 | 22 | int bitField; 23 | 24 | CharacteristicAccessType(int bitField) { 25 | this.bitField = bitField; 26 | } 27 | 28 | public int getBitField() { 29 | return bitField; 30 | } 31 | 32 | public static CharacteristicAccessType fromBitField(int bitField) { 33 | return Stream.of(CharacteristicAccessType.values()) 34 | .filter(c -> c.bitField == bitField) 35 | .findFirst().orElse(null); 36 | } 37 | 38 | public static Set parse(int flags) { 39 | return Stream.of(CharacteristicAccessType.values()) 40 | .filter(c -> (c.bitField & flags) > 0) 41 | .collect(Collectors.toSet()); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/Descriptor.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | /** 24 | * 25 | * @author Vlad Kolotov 26 | */ 27 | public interface Descriptor { 28 | 29 | byte[] readValue(); 30 | 31 | boolean writeValue(byte[] data); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/Device.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import org.sputnikdev.bluetooth.manager.BluetoothAddressType; 24 | 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | /** 29 | * 30 | * @author Vlad Kolotov 31 | */ 32 | public interface Device extends BluetoothObject { 33 | 34 | int getBluetoothClass(); 35 | 36 | boolean disconnect(); 37 | 38 | boolean connect(); 39 | 40 | String getName(); 41 | 42 | String getAlias(); 43 | 44 | void setAlias(String alias); 45 | 46 | boolean isBlocked(); 47 | 48 | boolean isBleEnabled(); 49 | 50 | void enableBlockedNotifications(Notification notification); 51 | 52 | void disableBlockedNotifications(); 53 | 54 | void setBlocked(boolean blocked); 55 | 56 | short getRSSI(); 57 | 58 | short getTxPower(); 59 | 60 | void enableRSSINotifications(Notification notification); 61 | 62 | void disableRSSINotifications(); 63 | 64 | boolean isConnected(); 65 | 66 | void enableConnectedNotifications(Notification notification); 67 | 68 | void disableConnectedNotifications(); 69 | 70 | boolean isServicesResolved(); 71 | 72 | void enableServicesResolvedNotifications(Notification notification); 73 | 74 | void disableServicesResolvedNotifications(); 75 | 76 | List getServices(); 77 | 78 | Map getServiceData(); 79 | 80 | Map getManufacturerData(); 81 | 82 | BluetoothAddressType getAddressType(); 83 | 84 | void enableServiceDataNotifications(Notification> notification); 85 | 86 | void disableServiceDataNotifications(); 87 | 88 | void enableManufacturerDataNotifications(Notification> notification); 89 | 90 | void disableManufacturerDataNotifications(); 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/Notification.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | 24 | /** 25 | * 26 | * @author Vlad Kolotov 27 | */ 28 | public interface Notification { 29 | 30 | void notify(T value); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/sputnikdev/bluetooth/manager/transport/Service.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | /*- 4 | * #%L 5 | * org.sputnikdev:bluetooth-manager 6 | * %% 7 | * Copyright (C) 2017 Sputnik Dev 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.util.List; 24 | 25 | /** 26 | * 27 | * @author Vlad Kolotov 28 | */ 29 | public interface Service extends BluetoothObject { 30 | 31 | List getCharacteristics(); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/auth/PinCodeAuthenticationProviderTest.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.auth; 2 | 3 | import org.junit.Before; 4 | import org.junit.Ignore; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.rules.ExpectedException; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.ArgumentCaptor; 10 | import org.mockito.Captor; 11 | import org.mockito.Mock; 12 | import org.mockito.Spy; 13 | import org.mockito.runners.MockitoJUnitRunner; 14 | import org.sputnikdev.bluetooth.URL; 15 | import org.sputnikdev.bluetooth.manager.BluetoothManager; 16 | import org.sputnikdev.bluetooth.manager.CharacteristicGovernor; 17 | import org.sputnikdev.bluetooth.manager.DeviceGovernor; 18 | import org.sputnikdev.bluetooth.manager.ValueListener; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.concurrent.Executors; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.function.Consumer; 26 | import java.util.function.Predicate; 27 | 28 | import static org.junit.Assert.assertEquals; 29 | import static org.junit.Assert.assertTrue; 30 | import static org.mockito.Matchers.any; 31 | import static org.mockito.Matchers.anyInt; 32 | import static org.mockito.Mockito.doAnswer; 33 | import static org.mockito.Mockito.doNothing; 34 | import static org.mockito.Mockito.times; 35 | import static org.mockito.Mockito.verify; 36 | import static org.mockito.Mockito.when; 37 | 38 | @RunWith(MockitoJUnitRunner.class) 39 | public class PinCodeAuthenticationProviderTest { 40 | 41 | private static final URL PIN_CODE_CHAR_URL = new URL("/XX:XX:XX:XX:XX:XX/11:22:33:44:55:66/eee1/eee3"); 42 | private static final int REFRESH_RATE = 5; 43 | private static final byte[] PIN_CODE = {0x11, 0x44}; 44 | private static final byte[] SUCCESSFUL_AUTH_RESPONSE = {0x77, 0x77}; 45 | private static final byte[] FAILED_AUTH_RESPONSE = {0x78, 0x78}; 46 | 47 | @Mock 48 | private BluetoothManager bluetoothManager; 49 | @Mock 50 | private DeviceGovernor deviceGovernor; 51 | @Mock 52 | private CharacteristicGovernor pinCodeCharacteristic; 53 | 54 | @Captor 55 | private ArgumentCaptor> authConditionCaptor; 56 | @Captor 57 | private ArgumentCaptor> authTaskCaptor; 58 | @Captor 59 | private ArgumentCaptor authResponseListenerCaptor; 60 | 61 | @Rule 62 | public ExpectedException expectedEx = ExpectedException.none(); 63 | 64 | private List> authFutures = new ArrayList<>(); 65 | 66 | @Spy 67 | private PinCodeAuthenticationProvider provider = 68 | new PinCodeAuthenticationProvider(PIN_CODE_CHAR_URL.getServiceUUID(), 69 | PIN_CODE_CHAR_URL.getCharacteristicUUID()); 70 | 71 | @Before 72 | public void setUp() { 73 | when(deviceGovernor.getURL()).thenReturn(PIN_CODE_CHAR_URL.getDeviceURL()); 74 | when(pinCodeCharacteristic.getURL()).thenReturn(PIN_CODE_CHAR_URL); 75 | when(bluetoothManager.getCharacteristicGovernor(PIN_CODE_CHAR_URL)).thenReturn(pinCodeCharacteristic); 76 | doNothing().when(pinCodeCharacteristic).addValueListener(authResponseListenerCaptor.capture()); 77 | when(bluetoothManager.getRefreshRate()).thenReturn(REFRESH_RATE); 78 | 79 | when(pinCodeCharacteristic.doWhen(authConditionCaptor.capture(), authTaskCaptor.capture())) 80 | .thenAnswer(answer -> { 81 | CompletableFuture future = new CompletableFuture<>(); 82 | authFutures.add(future); 83 | Executors.newScheduledThreadPool(1).schedule(() -> { 84 | try { 85 | answer.getArgumentAt(1, Consumer.class).accept(pinCodeCharacteristic); 86 | future.complete(null); 87 | } catch (Exception ex) { 88 | future.completeExceptionally(ex); 89 | } 90 | }, 200, TimeUnit.MILLISECONDS); 91 | return future; 92 | }); 93 | doNothing().when(provider).performAuthentication(any(CharacteristicGovernor.class), anyInt()); 94 | when(pinCodeCharacteristic.write(PIN_CODE)).thenReturn(true); 95 | when(pinCodeCharacteristic.isNotifiable()).thenReturn(true); 96 | } 97 | 98 | @Test 99 | public void testAuthenticate() { 100 | when(pinCodeCharacteristic.isNotifiable()).thenReturn(true); 101 | 102 | provider.authenticate(bluetoothManager, deviceGovernor); 103 | 104 | verify(provider).performAuthentication(pinCodeCharacteristic, REFRESH_RATE * 2); 105 | verify(pinCodeCharacteristic).doWhen(authConditionCaptor.getValue(), authTaskCaptor.getValue()); 106 | verify(pinCodeCharacteristic).addValueListener(authResponseListenerCaptor.getValue()); 107 | verify(bluetoothManager).getRefreshRate(); 108 | } 109 | 110 | @Test 111 | public void testAuthenticateTimeout() { 112 | when(bluetoothManager.getRefreshRate()).thenReturn(0); 113 | 114 | expectedEx.expect(BluetoothAuthenticationException.class); 115 | expectedEx.expectMessage("Could not authenticate. Timeout: " + PIN_CODE_CHAR_URL); 116 | 117 | try { 118 | provider.authenticate(bluetoothManager, deviceGovernor); 119 | } finally { 120 | assertEquals(1, authFutures.size()); 121 | assertTrue(authFutures.get(0).isCancelled()); 122 | } 123 | } 124 | 125 | @Test 126 | public void testAuthenticateException() { 127 | CompletableFuture authFuture = new CompletableFuture<>(); 128 | RuntimeException ex = new RuntimeException("Unexpected error"); 129 | when(pinCodeCharacteristic.doWhen(authConditionCaptor.capture(), authTaskCaptor.capture())).thenReturn(authFuture); 130 | Executors.newScheduledThreadPool(1).schedule(() -> authFuture.completeExceptionally(ex), 200, TimeUnit.MILLISECONDS); 131 | 132 | expectedEx.expect(BluetoothAuthenticationException.class); 133 | expectedEx.expectMessage("Could not authenticate: " + PIN_CODE_CHAR_URL + "; Error: java.lang.RuntimeException: Unexpected error"); 134 | 135 | provider.authenticate(bluetoothManager, deviceGovernor); 136 | } 137 | 138 | @Test 139 | public void testAuthenticateRepetitive() { 140 | CompletableFuture async = CompletableFuture.runAsync(() -> { 141 | provider.authenticate(bluetoothManager, deviceGovernor); 142 | }); 143 | provider.authenticate(bluetoothManager, deviceGovernor); 144 | provider.authenticate(bluetoothManager, deviceGovernor); 145 | 146 | async.join(); 147 | 148 | verify(provider, times(2)).performAuthentication(pinCodeCharacteristic, REFRESH_RATE * 2); 149 | } 150 | 151 | @Test 152 | public void testPerformAuthenticationWithoutResponse() { 153 | provider = new PinCodeAuthenticationProvider(PIN_CODE_CHAR_URL.getServiceUUID(), 154 | PIN_CODE_CHAR_URL.getCharacteristicUUID(), PIN_CODE, null); 155 | 156 | provider.performAuthentication(pinCodeCharacteristic, REFRESH_RATE * 2); 157 | 158 | verify(pinCodeCharacteristic).write(PIN_CODE); 159 | } 160 | 161 | @Test 162 | public void testPerformAuthenticationWithoutResponseFailToWrite() { 163 | provider = new PinCodeAuthenticationProvider(PIN_CODE_CHAR_URL.getServiceUUID(), 164 | PIN_CODE_CHAR_URL.getCharacteristicUUID(), PIN_CODE, null); 165 | when(pinCodeCharacteristic.write(PIN_CODE)).thenReturn(false); 166 | 167 | expectedEx.expect(BluetoothAuthenticationException.class); 168 | expectedEx.expectMessage("Could not send pin code: " + PIN_CODE_CHAR_URL); 169 | 170 | provider.performAuthentication(pinCodeCharacteristic, REFRESH_RATE * 2); 171 | } 172 | 173 | @Test 174 | public void testPerformAuthenticationWithResponse() throws Exception { 175 | provider = new PinCodeAuthenticationProvider(PIN_CODE_CHAR_URL.getServiceUUID(), 176 | PIN_CODE_CHAR_URL.getCharacteristicUUID(), PIN_CODE, SUCCESSFUL_AUTH_RESPONSE); 177 | 178 | when(pinCodeCharacteristic.isNotifiable()).thenReturn(true); 179 | doAnswer(answer -> { 180 | authResponseListenerCaptor.getValue().changed(SUCCESSFUL_AUTH_RESPONSE); 181 | return true; 182 | }).when(pinCodeCharacteristic).write(PIN_CODE); 183 | 184 | provider.authenticate(bluetoothManager, deviceGovernor); 185 | 186 | verify(pinCodeCharacteristic).write(PIN_CODE); 187 | } 188 | 189 | @Test 190 | public void testPerformAuthenticationWithResponseTimeout() { 191 | provider = new PinCodeAuthenticationProvider(PIN_CODE_CHAR_URL.getServiceUUID(), 192 | PIN_CODE_CHAR_URL.getCharacteristicUUID(), PIN_CODE, SUCCESSFUL_AUTH_RESPONSE); 193 | 194 | expectedEx.expect(BluetoothAuthenticationException.class); 195 | expectedEx.expectMessage("Could not receive auth response. Timeout happened: " + PIN_CODE_CHAR_URL); 196 | 197 | provider.performAuthentication(pinCodeCharacteristic, 0); 198 | } 199 | 200 | @Test 201 | public void testPerformAuthenticationWithResponseFailedResponse() throws Exception { 202 | provider = new PinCodeAuthenticationProvider(PIN_CODE_CHAR_URL.getServiceUUID(), 203 | PIN_CODE_CHAR_URL.getCharacteristicUUID(), PIN_CODE, SUCCESSFUL_AUTH_RESPONSE); 204 | 205 | when(pinCodeCharacteristic.isNotifiable()).thenReturn(true); 206 | doAnswer(answer -> { 207 | authResponseListenerCaptor.getValue().changed(FAILED_AUTH_RESPONSE); 208 | return true; 209 | }).when(pinCodeCharacteristic).write(PIN_CODE); 210 | 211 | expectedEx.expect(BluetoothAuthenticationException.class); 212 | expectedEx.expectMessage("Device sent unexpected authentication response. Not authorised: " + PIN_CODE_CHAR_URL); 213 | 214 | provider.authenticate(bluetoothManager, deviceGovernor); 215 | 216 | } 217 | 218 | } -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/impl/AbstractBluetoothObjectGovernorTest.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.InOrder; 7 | import org.mockito.InjectMocks; 8 | import org.mockito.Mock; 9 | import org.mockito.Spy; 10 | import org.mockito.internal.util.reflection.Whitebox; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | import org.sputnikdev.bluetooth.URL; 13 | import org.sputnikdev.bluetooth.manager.BluetoothInteractionException; 14 | import org.sputnikdev.bluetooth.manager.BluetoothObjectType; 15 | import org.sputnikdev.bluetooth.manager.BluetoothObjectVisitor; 16 | import org.sputnikdev.bluetooth.manager.GovernorListener; 17 | import org.sputnikdev.bluetooth.manager.transport.BluetoothObject; 18 | 19 | import java.time.Instant; 20 | import java.util.function.Function; 21 | 22 | import static org.junit.Assert.assertEquals; 23 | import static org.junit.Assert.assertFalse; 24 | import static org.junit.Assert.assertNotNull; 25 | import static org.junit.Assert.assertNull; 26 | import static org.junit.Assert.assertTrue; 27 | import static org.mockito.Matchers.any; 28 | import static org.mockito.Mockito.doThrow; 29 | import static org.mockito.Mockito.inOrder; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.never; 32 | import static org.mockito.Mockito.times; 33 | import static org.mockito.Mockito.verify; 34 | import static org.mockito.Mockito.verifyNoMoreInteractions; 35 | import static org.mockito.Mockito.when; 36 | import static org.powermock.api.mockito.PowerMockito.spy; 37 | 38 | @RunWith(MockitoJUnitRunner.class) 39 | public class AbstractBluetoothObjectGovernorTest { 40 | 41 | private static final URL URL= new URL("/11:22:33:44:55:66"); 42 | 43 | private BluetoothObject bluetoothObject = mock(BluetoothObject.class); 44 | private BluetoothManagerImpl bluetoothManager = mock(BluetoothManagerImpl.class); 45 | 46 | @Mock 47 | private GovernorListener governorListener; 48 | 49 | //@InjectMocks 50 | @Spy 51 | private AbstractBluetoothObjectGovernor governor = new AbstractBluetoothObjectGovernor(bluetoothManager, URL) { 52 | 53 | @Override 54 | public boolean isUpdatable() { 55 | return true; 56 | } 57 | 58 | @Override void reset(BluetoothObject object) { 59 | } 60 | 61 | @Override void update(BluetoothObject object) { 62 | } 63 | 64 | @Override void init(BluetoothObject object) { 65 | } 66 | 67 | @Override public BluetoothObjectType getType() { 68 | return BluetoothObjectType.ADAPTER; 69 | } 70 | 71 | @Override public void accept(BluetoothObjectVisitor visitor) throws Exception { 72 | } 73 | }; 74 | 75 | @Before 76 | public void setUp() { 77 | when(bluetoothObject.getURL()).thenReturn(URL.copyWithProtocol("tinyb")); 78 | when(bluetoothManager.getBluetoothObject(URL)).thenReturn(bluetoothObject); 79 | 80 | MockUtils.mockImplicitNotifications(bluetoothManager); 81 | } 82 | 83 | @Test(expected = BluetoothInteractionException.class) 84 | public void testInteractNotReady() { 85 | governor.interact("test", (obj) -> true); 86 | } 87 | 88 | @Test 89 | public void testInteractReady() { 90 | governor.update(); 91 | 92 | Function function = spy(new Function() { 93 | @Override 94 | public Boolean apply(BluetoothObject object) { 95 | assertEquals(bluetoothObject, object); 96 | return false; 97 | } 98 | }); 99 | governor.interact("test", function); 100 | verify(function).apply(bluetoothObject); 101 | } 102 | 103 | @Test(expected = Exception.class) 104 | public void testInteractException() { 105 | when(governor.isReady()).thenReturn(true); 106 | Function function = mock(Function.class); 107 | when(function.apply(any())).thenThrow(Exception.class); 108 | try { 109 | governor.interact("test", function); 110 | } finally { 111 | verify(governor).reset(); 112 | } 113 | } 114 | 115 | @Test 116 | public void testUpdateNotReady() throws Exception { 117 | Whitebox.setInternalState(governor, "bluetoothObject", null); 118 | when(bluetoothManager.getBluetoothObject(URL)).thenReturn(null); 119 | 120 | governor.update(); 121 | 122 | // check interactions 123 | InOrder inOrder = inOrder(governor, governorListener, bluetoothManager); 124 | 125 | //inOrder.verify(governor, times(1)).update(); 126 | inOrder.verify(bluetoothManager, times(1)).getBluetoothObject(URL); 127 | inOrder.verifyNoMoreInteractions(); 128 | } 129 | 130 | @Test 131 | public void testUpdateNotReadyToReady() throws Exception { 132 | // conditions 133 | Whitebox.setInternalState(governor, "bluetoothObject", null); 134 | governor.addGovernorListener(governorListener); 135 | 136 | governor.update(); 137 | 138 | InOrder inOrder = inOrder(governor, governorListener, bluetoothManager); 139 | inOrder.verify(bluetoothManager).getBluetoothObject(URL); 140 | inOrder.verify(governor).init(bluetoothObject); 141 | inOrder.verify(governorListener).ready(true); 142 | inOrder.verify(governorListener, never()).lastUpdatedChanged(any()); 143 | 144 | governor.updateLastInteracted(); 145 | governor.update(); 146 | inOrder.verify(governorListener).lastUpdatedChanged(any()); 147 | 148 | inOrder.verifyNoMoreInteractions(); 149 | } 150 | 151 | @Test 152 | public void testUpdateReadyToNotReady() throws Exception { 153 | // conditions 154 | doThrow(Exception.class).when(governor).update(bluetoothObject); 155 | governor.addGovernorListener(governorListener); 156 | 157 | // invocation 158 | governor.update(); 159 | 160 | // check interactions 161 | InOrder inOrder = inOrder(governor, governorListener); 162 | inOrder.verify(governor, times(1)).update(bluetoothObject); 163 | inOrder.verify(governor, times(1)).reset(bluetoothObject); 164 | inOrder.verify(governorListener, times(1)).ready(false); 165 | 166 | inOrder.verifyNoMoreInteractions(); 167 | assertNull(Whitebox.getInternalState(governor, "bluetoothObject")); 168 | } 169 | 170 | @Test 171 | public void testGetURL() throws Exception { 172 | assertEquals(URL, governor.getURL()); 173 | verify(governor, times(1)).getURL(); 174 | verifyNoMoreInteractions(governor); 175 | } 176 | 177 | @Test 178 | public void testAddGovernorListener() throws Exception { 179 | governor.addGovernorListener(governorListener); 180 | verify(governor, times(1)).addGovernorListener(governorListener); 181 | verifyNoMoreInteractions(governor); 182 | } 183 | 184 | @Test 185 | public void testRemoveGovernorListener() throws Exception { 186 | governor.removeGovernorListener(governorListener); 187 | verify(governor, times(1)).removeGovernorListener(governorListener); 188 | verifyNoMoreInteractions(governor); 189 | } 190 | 191 | @Test 192 | public void testUpdateLastChanged() throws Exception { 193 | governor.addGovernorListener(governorListener); 194 | 195 | Instant lastChanged = governor.getLastInteracted(); 196 | assertNull(lastChanged); 197 | 198 | Thread.sleep(1); 199 | governor.updateLastInteracted(); 200 | 201 | lastChanged = governor.getLastInteracted(); 202 | assertNotNull(lastChanged); 203 | 204 | Thread.sleep(1); 205 | governor.updateLastInteracted(); 206 | 207 | assertTrue(lastChanged.isBefore(governor.getLastInteracted())); 208 | } 209 | 210 | @Test 211 | public void testResetNotReady() throws Exception { 212 | Whitebox.setInternalState(governor, "bluetoothObject", null); 213 | governor.addGovernorListener(governorListener); 214 | 215 | governor.reset(); 216 | 217 | // check interactions 218 | InOrder inOrder = inOrder(governor, governorListener); 219 | // side effect of using spy 220 | inOrder.verify(governor, times(1)).addGovernorListener(governorListener); 221 | inOrder.verify(governor).reset(); 222 | 223 | // actual verification 224 | inOrder.verifyNoMoreInteractions(); 225 | } 226 | 227 | @Test 228 | public void testResetReady() throws Exception { 229 | governor.addGovernorListener(governorListener); 230 | 231 | governor.update(); 232 | 233 | governor.reset(); 234 | 235 | // check interactions 236 | InOrder inOrder = inOrder(governor, governorListener, bluetoothManager); 237 | inOrder.verify(bluetoothManager).resetDescendants(URL); 238 | inOrder.verify(governorListener, times(1)).ready(false); 239 | } 240 | 241 | @Test 242 | public void testIsReady() { 243 | assertFalse(governor.isReady()); 244 | 245 | governor.update(); 246 | 247 | assertTrue(governor.isReady()); 248 | } 249 | 250 | @Test 251 | public void testNotifyLastChanged() { 252 | Instant date = Instant.now(); 253 | Whitebox.setInternalState(governor, "lastInteracted", date); 254 | governor.addGovernorListener(governorListener); 255 | 256 | governor.notifyLastChanged(); 257 | 258 | verify(governorListener, times(1)).lastUpdatedChanged(date); 259 | } 260 | 261 | @Test 262 | public void testNotifyLastChangedException() { 263 | Instant date = Instant.now(); 264 | Whitebox.setInternalState(governor, "lastInteracted", date); 265 | governor.addGovernorListener(governorListener); 266 | doThrow(Exception.class).when(governorListener).lastUpdatedChanged(any()); 267 | 268 | governor.notifyLastChanged(); 269 | 270 | verify(governorListener, times(1)).lastUpdatedChanged(date); 271 | } 272 | 273 | @Test 274 | public void testNotifyReady() { 275 | governor.addGovernorListener(governorListener); 276 | 277 | governor.notifyReady(true); 278 | 279 | verify(governorListener, times(1)).ready(true); 280 | } 281 | 282 | @Test 283 | public void testNotifyReadyException() { 284 | governor.addGovernorListener(governorListener); 285 | doThrow(Exception.class).when(governorListener).ready(true); 286 | 287 | governor.notifyReady(true); 288 | 289 | verify(governorListener, times(1)).ready(true); 290 | } 291 | 292 | } 293 | -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/impl/BluetoothManagerIT.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | import org.junit.Test; 4 | import org.sputnikdev.bluetooth.URL; 5 | import org.sputnikdev.bluetooth.manager.BluetoothManager; 6 | import org.sputnikdev.bluetooth.manager.CombinedGovernor; 7 | import org.sputnikdev.bluetooth.manager.DeviceGovernor; 8 | import org.sputnikdev.bluetooth.manager.auth.PinCodeAuthenticationProvider; 9 | import org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType; 10 | import org.sputnikdev.bluetooth.manager.transport.Device; 11 | import org.sputnikdev.bluetooth.manager.util.AdapterEmulator; 12 | import org.sputnikdev.bluetooth.manager.util.BluetoothFactoryEmulator; 13 | import org.sputnikdev.bluetooth.manager.util.CharacteristicEmulator; 14 | import org.sputnikdev.bluetooth.manager.util.DeviceEmulator; 15 | 16 | import static org.junit.Assert.assertNotNull; 17 | import static org.junit.Assert.assertTrue; 18 | import static org.mockito.Matchers.any; 19 | import static org.mockito.Mockito.atLeastOnce; 20 | import static org.mockito.Mockito.never; 21 | import static org.mockito.Mockito.verify; 22 | 23 | public class BluetoothManagerIT { 24 | 25 | private static final URL ADAPTER_URL = new URL("bluegiga:/12:34:56:78:90:12"); 26 | private static final URL DEVICE_URL = ADAPTER_URL.copyWithDevice("4C:65:00:D0:7A:EE"); 27 | private static final URL PIN_CODE_CHAR_URL = DEVICE_URL.copyWith("0000eee1-0000-1000-8000-00805f9b34fb", 28 | "0000eee3-0000-1000-8000-00805f9b34fb"); 29 | public static final byte[] PIN_CODE = { 0x11, 0x44 }; 30 | public static final byte[] SUCCESSFUL_AUTH_RESPONSE = { 0x77, 0x77 }; 31 | public static final byte[] FAILED_AUTH_RESPONSE = { 0x78, 0x78 }; 32 | 33 | private BluetoothManager bluetoothManager = new BluetoothManagerBuilder() 34 | .withDiscovering(true) 35 | .withRediscover(true) 36 | .withCombinedDevices(true) 37 | .withRefreshRate(1) 38 | .withDiscoveryRate(1) 39 | .build(); 40 | 41 | @Test 42 | public void testConnectAndAuthenticate() throws Exception { 43 | 44 | BluetoothFactoryEmulator factory = new BluetoothFactoryEmulator("bluegiga"); 45 | DeviceEmulator deviceEmulator = factory.addAdapter(ADAPTER_URL).addDevice(DEVICE_URL); 46 | deviceEmulator.scheduleRandomRSSI(1); 47 | CharacteristicEmulator pinCodeChar = 48 | deviceEmulator.addCharacteristic(PIN_CODE_CHAR_URL, 49 | CharacteristicAccessType.NOTIFY, CharacteristicAccessType.WRITE_WITHOUT_RESPONSE); 50 | pinCodeChar.whenWritten(PIN_CODE, () -> pinCodeChar.notify(SUCCESSFUL_AUTH_RESPONSE)); 51 | 52 | bluetoothManager.registerFactory(factory.getFactory()); 53 | 54 | DeviceGovernor deviceGovernor = bluetoothManager.getDeviceGovernor(DEVICE_URL.copyWithAdapter(CombinedGovernor.COMBINED_ADDRESS)); 55 | assertNotNull(deviceGovernor); 56 | 57 | deviceGovernor.setAuthenticationProvider(new PinCodeAuthenticationProvider(PIN_CODE_CHAR_URL.getServiceUUID(), 58 | PIN_CODE_CHAR_URL.getCharacteristicUUID(), PIN_CODE, SUCCESSFUL_AUTH_RESPONSE)); 59 | 60 | deviceGovernor.setConnectionControl(true); 61 | 62 | deviceGovernor.doWhen(DeviceGovernor::isAuthenticated, gov -> { 63 | assertTrue(deviceGovernor.isAuthenticated()); 64 | }).join(); 65 | 66 | // verify 67 | Device device = deviceEmulator.getDevice(); 68 | verify(device).getAlias(); 69 | verify(device).getName(); 70 | verify(device).isBleEnabled(); 71 | verify(device).getBluetoothClass(); 72 | 73 | verify(device).enableConnectedNotifications(any()); 74 | verify(device).enableServicesResolvedNotifications(any()); 75 | verify(device).enableBlockedNotifications(any()); 76 | verify(device).enableRSSINotifications(any()); 77 | verify(device).enableServiceDataNotifications(any()); 78 | verify(device).enableManufacturerDataNotifications(any()); 79 | 80 | verify(device, atLeastOnce()).getServices(); 81 | verify(device, atLeastOnce()).getTxPower(); 82 | verify(device, atLeastOnce()).getRSSI(); 83 | verify(device, atLeastOnce()).isBlocked(); 84 | verify(device, atLeastOnce()).isConnected(); 85 | verify(device, atLeastOnce()).isServicesResolved(); 86 | 87 | } 88 | 89 | @Test 90 | public void testBeaconDynamicDeviceAddress() throws Exception { 91 | 92 | URL privateDeviceURL = new URL("/XX:XX:XX:XX:XX:XX/[name=PrivateName]"); 93 | 94 | BluetoothFactoryEmulator factory = new BluetoothFactoryEmulator("bluegiga"); 95 | 96 | AdapterEmulator adapterEmulator = factory.addAdapter(ADAPTER_URL); 97 | 98 | // Non resolvable device MAC 99 | DeviceEmulator deviceEmulator = 100 | adapterEmulator.addDevice(ADAPTER_URL.copyWithDevice("5C:80:AE:CA:01:DE"), "PrivateName"); 101 | 102 | deviceEmulator.scheduleRandomRSSI(1); 103 | 104 | bluetoothManager.registerFactory(factory.getFactory()); 105 | 106 | 107 | bluetoothManager.addDeviceDiscoveryListener(device -> { 108 | System.out.println(device.getURL() + " " + device.getRSSI()); 109 | }); 110 | 111 | DeviceGovernor privateDevice = bluetoothManager.getDeviceGovernor(privateDeviceURL); 112 | 113 | privateDevice.setOnlineTimeout(2); 114 | 115 | privateDevice.doWhen(DeviceGovernor::isOnline, gov -> { 116 | assertTrue(privateDevice.isOnline()); 117 | }).join(); 118 | 119 | // this would indicate that the governor was never reset 120 | verify(deviceEmulator.getDevice(), never()).disableRSSINotifications(); 121 | 122 | // now, native device changes its address 123 | deviceEmulator.cancelRSSI(); 124 | // Also non resolvable device MAC 125 | DeviceEmulator changedMacDevice = 126 | adapterEmulator.addDevice(ADAPTER_URL.copyWithDevice("73:11:20:83:78:09"), "PrivateName"); 127 | changedMacDevice.scheduleRandomRSSI(1); 128 | 129 | Thread.sleep(3000); 130 | 131 | // check if we still online 132 | assertTrue(privateDevice.isOnline()); 133 | 134 | // this would indicate that the governor was reset after the device changed its address 135 | verify(deviceEmulator.getDevice()).disableRSSINotifications(); 136 | 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/impl/CombinedCharacteristicGovernorImplTest.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.ArgumentCaptor; 7 | import org.mockito.Captor; 8 | import org.mockito.Mock; 9 | import org.mockito.runners.MockitoJUnitRunner; 10 | import org.sputnikdev.bluetooth.URL; 11 | import org.sputnikdev.bluetooth.manager.CharacteristicGovernor; 12 | import org.sputnikdev.bluetooth.manager.DiscoveredAdapter; 13 | import org.sputnikdev.bluetooth.manager.GovernorListener; 14 | import org.sputnikdev.bluetooth.manager.ManagerListener; 15 | import org.sputnikdev.bluetooth.manager.ValueListener; 16 | 17 | import java.time.Instant; 18 | import java.util.Arrays; 19 | import java.util.HashSet; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.concurrent.ExecutionException; 22 | 23 | import static org.junit.Assert.assertEquals; 24 | import static org.junit.Assert.assertFalse; 25 | import static org.junit.Assert.assertNotNull; 26 | import static org.junit.Assert.assertTrue; 27 | import static org.junit.Assert.fail; 28 | import static org.mockito.Matchers.any; 29 | import static org.mockito.Mockito.atLeastOnce; 30 | import static org.mockito.Mockito.atMost; 31 | import static org.mockito.Mockito.doAnswer; 32 | import static org.mockito.Mockito.doNothing; 33 | import static org.mockito.Mockito.mock; 34 | import static org.mockito.Mockito.spy; 35 | import static org.mockito.Mockito.times; 36 | import static org.mockito.Mockito.verify; 37 | import static org.mockito.Mockito.verifyNoMoreInteractions; 38 | import static org.mockito.Mockito.verifyZeroInteractions; 39 | import static org.mockito.Mockito.when; 40 | 41 | @RunWith(MockitoJUnitRunner.class) 42 | public class CombinedCharacteristicGovernorImplTest { 43 | 44 | private static final URL URL = new URL("/XX:XX:XX:XX:XX:XX/12:34:56:78:90:12"); 45 | private static final String SERVICE = "0000180f-0000-1000-8000-00805f9b34fb"; 46 | private static final URL CHARACTERISTIC_URL = URL.copyWith(SERVICE, "00002a19-0000-1000-8000-00805f9b34fb"); 47 | private static final URL ADAPTER_1 = new URL("/11:11:11:11:11:11"); 48 | private static final URL ADAPTER_2 = new URL("/22:22:22:22:22:22"); 49 | private static final URL CHARACTERISTIC_1 = CHARACTERISTIC_URL.copyWithAdapter(ADAPTER_1.getAdapterAddress()); 50 | private static final URL CHARACTERISTIC_2 = CHARACTERISTIC_URL.copyWithAdapter(ADAPTER_2.getAdapterAddress()); 51 | private static final Instant LAST_NOTIFIED = Instant.now().minusSeconds(2); 52 | private static final Instant LAST_INTERACTED = Instant.now().minusSeconds(1); 53 | 54 | 55 | private BluetoothManagerImpl bluetoothManager = mock(BluetoothManagerImpl.class); 56 | 57 | @Mock 58 | private CharacteristicGovernor delegate1; 59 | @Mock 60 | private CharacteristicGovernor delegate2; 61 | @Mock 62 | private DiscoveredAdapter adapter1; 63 | @Mock 64 | private DiscoveredAdapter adapter2; 65 | @Mock 66 | private GovernorListener governorListener; 67 | @Mock 68 | private ValueListener valueListener; 69 | 70 | @Captor 71 | private ArgumentCaptor managerListenerArgumentCaptor; 72 | 73 | //@Spy 74 | private CombinedCharacteristicGovernorImpl governor = new CombinedCharacteristicGovernorImpl(bluetoothManager, CHARACTERISTIC_URL); 75 | 76 | 77 | @Before 78 | public void setUp() { 79 | when(delegate1.getURL()).thenReturn(CHARACTERISTIC_URL.copyWithAdapter(ADAPTER_1.getAdapterAddress())); 80 | when(delegate2.getURL()).thenReturn(CHARACTERISTIC_URL.copyWithAdapter(ADAPTER_2.getAdapterAddress())); 81 | 82 | when(delegate2.getLastNotified()).thenReturn(LAST_NOTIFIED); 83 | when(delegate2.getLastInteracted()).thenReturn(LAST_INTERACTED); 84 | 85 | when(adapter1.getURL()).thenReturn(ADAPTER_1); 86 | when(adapter2.getURL()).thenReturn(ADAPTER_2); 87 | 88 | when(bluetoothManager.getDiscoveredAdapters()).thenReturn(new HashSet<>(Arrays.asList(adapter1, adapter2))); 89 | when(bluetoothManager.getCharacteristicGovernor(CHARACTERISTIC_1)).thenReturn(delegate1); 90 | when(bluetoothManager.getCharacteristicGovernor(CHARACTERISTIC_2)).thenReturn(delegate2); 91 | 92 | governor.addValueListener(valueListener); 93 | governor.addGovernorListener(governorListener); 94 | 95 | doNothing().when(bluetoothManager).addManagerListener(managerListenerArgumentCaptor.capture()); 96 | 97 | doAnswer(answer -> { 98 | ((Runnable) answer.getArguments()[0]).run(); 99 | return null; 100 | }).when(bluetoothManager).notify(any(Runnable.class)); 101 | } 102 | 103 | @Test 104 | public void testInit() { 105 | CombinedCharacteristicGovernorImpl spy = spy(governor); 106 | 107 | spy.init(); 108 | 109 | assertNotNull(managerListenerArgumentCaptor.getValue()); 110 | verify(bluetoothManager).addManagerListener(managerListenerArgumentCaptor.getValue()); 111 | verify(bluetoothManager).getCharacteristicGovernor(CHARACTERISTIC_1); 112 | verify(bluetoothManager).getCharacteristicGovernor(CHARACTERISTIC_2); 113 | verify(bluetoothManager).getDiscoveredAdapters(); 114 | verify(delegate1).isReady(); 115 | verify(delegate2).isReady(); 116 | verify(spy).update(); 117 | 118 | verifyNoMoreInteractions(bluetoothManager, delegate1, delegate2, governorListener, valueListener); 119 | } 120 | 121 | @Test 122 | public void testInitDelegatesNotReady() { 123 | CombinedCharacteristicGovernorImpl spy = spy(governor); 124 | 125 | CompletableFuture ready = governor.whenReady(gov -> { 126 | fail(); 127 | return null; 128 | }); 129 | 130 | spy.init(); 131 | 132 | assertFalse(ready.isDone()); 133 | 134 | verify(spy).update(); 135 | verify(delegate1).isReady(); 136 | verify(delegate2).isReady(); 137 | verify(bluetoothManager).addManagerListener(managerListenerArgumentCaptor.getValue()); 138 | verify(bluetoothManager).getCharacteristicGovernor(CHARACTERISTIC_1); 139 | verify(bluetoothManager).getCharacteristicGovernor(CHARACTERISTIC_2); 140 | verify(bluetoothManager).getDiscoveredAdapters(); 141 | 142 | verifyNoMoreInteractions(bluetoothManager, delegate1, delegate2, governorListener, valueListener); 143 | } 144 | 145 | @Test 146 | public void testInitDelegatesReady() throws ExecutionException, InterruptedException { 147 | CombinedCharacteristicGovernorImpl spy = spy(governor); 148 | when(delegate2.isReady()).thenReturn(true); 149 | 150 | CompletableFuture ready = spy.whenReady(gov -> true); 151 | 152 | assertFalse(ready.isDone()); 153 | assertFalse(spy.isReady()); 154 | 155 | spy.init(); 156 | 157 | verify(spy).update(); 158 | // 2 times: while installing delegate, while completing future 159 | verify(delegate2, atLeastOnce()).isReady(); 160 | 161 | assertDelegateInstallation(delegate2); 162 | 163 | assertTrue(spy.isReady()); 164 | assertEquals(LAST_INTERACTED, spy.getLastInteracted()); 165 | assertEquals(LAST_NOTIFIED, spy.getLastNotified()); 166 | assertTrue(ready.get()); 167 | assertTrue(ready.isDone()); 168 | 169 | verify(delegate2, atLeastOnce()).getLastNotified(); 170 | verify(delegate2, atLeastOnce()).getLastInteracted(); 171 | verify(delegate2, atLeastOnce()).isReady(); 172 | 173 | verify(bluetoothManager).addManagerListener(managerListenerArgumentCaptor.getValue()); 174 | verify(bluetoothManager, atMost(1)).getCharacteristicGovernor(CHARACTERISTIC_1); 175 | verify(bluetoothManager).getCharacteristicGovernor(CHARACTERISTIC_2); 176 | verify(bluetoothManager).getDiscoveredAdapters(); 177 | verify(bluetoothManager).notify(any(Runnable.class)); 178 | 179 | verify(governorListener).ready(true); 180 | verify(governorListener).lastUpdatedChanged(LAST_INTERACTED); 181 | 182 | verifyNoMoreInteractions(bluetoothManager, governorListener, valueListener); 183 | } 184 | 185 | @Test 186 | public void testUpdateDelegatesBecomeReady() throws ExecutionException, InterruptedException { 187 | CompletableFuture ready = governor.whenReady(gov -> { return true; }); 188 | assertFalse(ready.isDone()); 189 | 190 | governor.init(); 191 | 192 | governor.update(); 193 | 194 | assertFalse(governor.isReady()); 195 | assertFalse(ready.isDone()); 196 | 197 | verify(delegate1, times(2)).isReady(); 198 | verify(delegate2, times(2)).isReady(); 199 | 200 | when(delegate2.isReady()).thenReturn(true); 201 | 202 | assertFalse(governor.isReady()); 203 | verifyZeroInteractions(valueListener, governorListener); 204 | 205 | governor.update(); 206 | 207 | assertTrue(governor.isReady()); 208 | assertDelegateInstallation(delegate2); 209 | 210 | assertTrue(ready.isDone()); 211 | assertTrue(ready.get()); 212 | 213 | verify(bluetoothManager).addManagerListener(managerListenerArgumentCaptor.getValue()); 214 | verify(bluetoothManager, atLeastOnce()).getCharacteristicGovernor(CHARACTERISTIC_1); 215 | verify(bluetoothManager, atLeastOnce()).getCharacteristicGovernor(CHARACTERISTIC_2); 216 | verify(bluetoothManager, atLeastOnce()).getDiscoveredAdapters(); 217 | verify(bluetoothManager).notify(any(Runnable.class)); 218 | 219 | verify(delegate1, atLeastOnce()).isReady(); 220 | verify(delegate2, atLeastOnce()).isReady(); 221 | 222 | verify(governorListener).ready(true); 223 | verify(governorListener).lastUpdatedChanged(LAST_INTERACTED); 224 | 225 | verifyNoMoreInteractions(bluetoothManager, delegate1, governorListener, valueListener); 226 | } 227 | 228 | @Test 229 | public void testDelegateManagerListener() { 230 | assertFalse(governor.isReady()); 231 | governor.init(); 232 | assertFalse(governor.isReady()); 233 | 234 | when(delegate1.isReady()).thenReturn(true); 235 | managerListenerArgumentCaptor.getValue().ready(delegate1, true); 236 | 237 | assertTrue(governor.isReady()); 238 | assertDelegateInstallation(delegate1); 239 | 240 | when(delegate1.isReady()).thenReturn(false); 241 | managerListenerArgumentCaptor.getValue().ready(delegate1, false); 242 | assertDelegateRemoval(delegate1); 243 | 244 | assertFalse(governor.isReady()); 245 | } 246 | 247 | private void assertDelegateInstallation(CharacteristicGovernor delegate) { 248 | verify(delegate).addValueListener(valueListener); 249 | verify(delegate).addGovernorListener(governorListener); 250 | verify(delegate).getLastNotified(); 251 | verify(delegate).getLastInteracted(); 252 | verify(delegate, atLeastOnce()).isReady(); 253 | } 254 | 255 | private void assertDelegateRemoval(CharacteristicGovernor delegate) { 256 | verify(delegate).removeValueListener(valueListener); 257 | verify(delegate).removeGovernorListener(governorListener); 258 | verify(delegate, atLeastOnce()).getLastNotified(); 259 | verify(delegate, atLeastOnce()).getLastInteracted(); 260 | verify(delegate, atLeastOnce()).isReady(); 261 | } 262 | 263 | } -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/impl/MockUtils.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.impl; 2 | 3 | import org.slf4j.Logger; 4 | 5 | import java.util.List; 6 | import java.util.function.BiConsumer; 7 | 8 | import static org.mockito.Matchers.any; 9 | import static org.mockito.Matchers.anyList; 10 | import static org.mockito.Matchers.anyString; 11 | import static org.mockito.Mockito.doAnswer; 12 | 13 | public class MockUtils { 14 | 15 | public static void mockImplicitNotifications(BluetoothManagerImpl bluetoothManager) { 16 | doAnswer(answer -> { 17 | try { 18 | ((Runnable) answer.getArguments()[0]).run(); 19 | } catch (Exception ignore) { } 20 | return null; 21 | }).when(bluetoothManager).notify(any(Runnable.class)); 22 | doAnswer(answer -> { 23 | List listeners = (List) answer.getArguments()[0]; 24 | Object value = answer.getArguments()[2]; 25 | BiConsumer consumer = (BiConsumer) answer.getArguments()[1]; 26 | listeners.forEach(listener -> { 27 | try { 28 | consumer.accept(listener, value); 29 | } catch (Exception ignore) { } 30 | }); 31 | return null; 32 | }).when(bluetoothManager).notify(anyList(), any(BiConsumer.class), any(), 33 | any(Logger.class), anyString()); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/transport/CharacteristicAccessTypeTest.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.transport; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Set; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertTrue; 9 | import static org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType.*; 10 | 11 | 12 | public class CharacteristicAccessTypeTest { 13 | 14 | @Test 15 | public void testGetBitField() throws Exception { 16 | assertEquals(0x01, BROADCAST.getBitField()); 17 | assertEquals(0x02, READ.getBitField()); 18 | assertEquals(0x04, WRITE_WITHOUT_RESPONSE.getBitField()); 19 | assertEquals(0x08, WRITE.getBitField()); 20 | assertEquals(0x10, NOTIFY.getBitField()); 21 | assertEquals(0x20, INDICATE.getBitField()); 22 | assertEquals(0x40, AUTHENTICATED_SIGNED_WRITES.getBitField()); 23 | assertEquals(0x80, EXTENDED_PROPERTIES.getBitField()); 24 | } 25 | 26 | @Test 27 | public void testFromBitField() throws Exception { 28 | assertEquals(BROADCAST, fromBitField(0b00000001)); 29 | assertEquals(READ, fromBitField(0b00000010)); 30 | assertEquals(WRITE_WITHOUT_RESPONSE, fromBitField(0b00000100)); 31 | assertEquals(WRITE, fromBitField(0b00001000)); 32 | assertEquals(NOTIFY, fromBitField(0b00010000)); 33 | assertEquals(INDICATE, fromBitField(0b00100000)); 34 | assertEquals(AUTHENTICATED_SIGNED_WRITES, fromBitField(0b01000000)); 35 | assertEquals(EXTENDED_PROPERTIES, fromBitField(0b10000000)); 36 | } 37 | 38 | @Test 39 | public void testParse() throws Exception { 40 | int notify = 0b00010000; 41 | Set actual = parse(notify); 42 | assertEquals(1, actual.size()); 43 | assertTrue(actual.contains(NOTIFY)); 44 | 45 | int notifyAndRead = 0b00010010; 46 | actual = parse(notifyAndRead); 47 | assertEquals(2, actual.size()); 48 | assertTrue(actual.contains(NOTIFY)); 49 | assertTrue(actual.contains(READ)); 50 | 51 | int notifyReadAndWrite = 0b00011010; 52 | actual = parse(notifyReadAndWrite); 53 | assertEquals(3, actual.size()); 54 | assertTrue(actual.contains(NOTIFY)); 55 | assertTrue(actual.contains(READ)); 56 | assertTrue(actual.contains(WRITE)); 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/util/AdapterEmulator.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.util; 2 | 3 | import org.sputnikdev.bluetooth.URL; 4 | import org.sputnikdev.bluetooth.manager.transport.Adapter; 5 | 6 | import java.util.Collection; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.reset; 14 | import static org.mockito.Mockito.when; 15 | 16 | public class AdapterEmulator { 17 | 18 | private Adapter adapter; 19 | private Map devices = new HashMap<>(); 20 | 21 | public AdapterEmulator(URL url) { 22 | this(url, url.getAdapterAddress()); 23 | } 24 | 25 | public AdapterEmulator(URL url, String name) { 26 | adapter = mock(Adapter.class); 27 | when(adapter.getURL()).thenReturn(url); 28 | when(adapter.getName()).thenReturn(name); 29 | when(adapter.getDevices()).thenAnswer(answer -> devices.values().stream() 30 | .map(DeviceEmulator::getDevice).collect(Collectors.toList())); 31 | when(adapter.isPowered()).thenReturn(true); 32 | } 33 | 34 | public DeviceEmulator addDevice(URL url, String name) { 35 | DeviceEmulator deviceEmulator = new DeviceEmulator(url, name); 36 | devices.put(url, deviceEmulator); 37 | return deviceEmulator; 38 | } 39 | 40 | public DeviceEmulator addDevice(URL url) { 41 | return addDevice(url, url.getDeviceAddress()); 42 | } 43 | 44 | public Adapter getAdapter() { 45 | return adapter; 46 | } 47 | 48 | public Collection getDeviceEmulators() { 49 | return devices.values(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/util/BluetoothFactoryEmulator.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.util; 2 | 3 | import org.sputnikdev.bluetooth.URL; 4 | import org.sputnikdev.bluetooth.manager.DiscoveredAdapter; 5 | import org.sputnikdev.bluetooth.manager.transport.BluetoothObjectFactory; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Random; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.ScheduledExecutorService; 12 | import java.util.stream.Collectors; 13 | 14 | import static org.mockito.Matchers.any; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class BluetoothFactoryEmulator { 19 | 20 | private BluetoothObjectFactory objectFactory; 21 | private Map adapters = new HashMap<>(); 22 | 23 | static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10); 24 | static final Random random = new Random(); 25 | 26 | public BluetoothFactoryEmulator(String factoryName) { 27 | objectFactory = mock(BluetoothObjectFactory.class); 28 | when(objectFactory.getProtocolName()).thenReturn(factoryName); 29 | when(objectFactory.getDiscoveredAdapters()).thenAnswer(answer -> adapters.keySet()); 30 | 31 | when(objectFactory.getDiscoveredDevices()).thenAnswer(answer -> adapters.values().stream() 32 | .flatMap(adapter -> adapter.getDeviceEmulators().stream()) 33 | .map(DeviceEmulator::getDiscoveredDevice).collect(Collectors.toSet())); 34 | 35 | when(objectFactory.getAdapter(any(URL.class))).thenAnswer(answer -> adapters.values().stream() 36 | .filter(adapter -> adapter.getAdapter().getURL().equals(answer.getArgumentAt(0, URL.class))) 37 | .findFirst().map(AdapterEmulator::getAdapter).orElse(null)); 38 | 39 | when(objectFactory.getDevice(any(URL.class))).thenAnswer(answer -> adapters.values().stream() 40 | .flatMap(adapter -> adapter.getAdapter().getDevices().stream()) 41 | .filter(device -> device.getURL().equals((answer.getArgumentAt(0, URL.class)))) 42 | .findFirst().orElse(null)); 43 | 44 | when(objectFactory.getCharacteristic(any(URL.class))).thenAnswer(answer -> adapters.values().stream() 45 | .flatMap(adapter -> adapter.getAdapter().getDevices().stream()) 46 | .flatMap(device -> device.getServices().stream()) 47 | .flatMap(service -> service.getCharacteristics().stream()) 48 | .filter(characteristic -> characteristic.getURL().equals((answer.getArgumentAt(0, URL.class)))) 49 | .findFirst().orElse(null)); 50 | } 51 | 52 | public AdapterEmulator addAdapter(URL url) { 53 | return addAdapter(url, url.getAdapterAddress()); 54 | } 55 | 56 | public AdapterEmulator addAdapter(URL url, String name) { 57 | AdapterEmulator adapterEmulator = new AdapterEmulator(url, name); 58 | DiscoveredAdapter discoveredAdapter = new DiscoveredAdapter(url, name, null); 59 | adapters.put(discoveredAdapter, adapterEmulator); 60 | return adapterEmulator; 61 | } 62 | 63 | public BluetoothObjectFactory getFactory() { 64 | return objectFactory; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/util/CharacteristicEmulator.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.util; 2 | 3 | import org.mockito.ArgumentCaptor; 4 | import org.mockito.Mockito; 5 | import org.sputnikdev.bluetooth.URL; 6 | import org.sputnikdev.bluetooth.manager.transport.Characteristic; 7 | import org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType; 8 | import org.sputnikdev.bluetooth.manager.transport.Notification; 9 | 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.when; 16 | 17 | public class CharacteristicEmulator { 18 | 19 | private Characteristic characteristic; 20 | private ArgumentCaptor valueNotificationCaptor = ArgumentCaptor.forClass(Notification.class); 21 | 22 | public CharacteristicEmulator(URL url, CharacteristicAccessType... flags) { 23 | characteristic = mock(Characteristic.class); 24 | when(characteristic.getURL()).thenReturn(url); 25 | when(characteristic.getFlags()).thenReturn(Stream.of(flags).collect(Collectors.toSet())); 26 | 27 | Mockito.doAnswer(answer -> { 28 | when(characteristic.isNotifying()).thenReturn(true); 29 | return true; 30 | }).when(characteristic).enableValueNotifications(valueNotificationCaptor.capture()); 31 | } 32 | 33 | public Characteristic getCharacteristic() { 34 | return characteristic; 35 | } 36 | 37 | public void whenWritten(byte[] data, Runnable then) { 38 | when(characteristic.writeValue(data)).thenAnswer(answer -> { 39 | CompletableFuture.runAsync(then); 40 | return true; 41 | }); 42 | } 43 | 44 | public void notify(byte[] value) { 45 | valueNotificationCaptor.getValue().notify(value); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/org/sputnikdev/bluetooth/manager/util/DeviceEmulator.java: -------------------------------------------------------------------------------- 1 | package org.sputnikdev.bluetooth.manager.util; 2 | 3 | import org.mockito.ArgumentCaptor; 4 | import org.sputnikdev.bluetooth.URL; 5 | import org.sputnikdev.bluetooth.manager.DiscoveredDevice; 6 | import org.sputnikdev.bluetooth.manager.transport.Characteristic; 7 | import org.sputnikdev.bluetooth.manager.transport.CharacteristicAccessType; 8 | import org.sputnikdev.bluetooth.manager.transport.Device; 9 | import org.sputnikdev.bluetooth.manager.transport.Notification; 10 | import org.sputnikdev.bluetooth.manager.transport.Service; 11 | 12 | import java.util.Collection; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.ScheduledFuture; 18 | import java.util.concurrent.TimeUnit; 19 | import java.util.stream.Collectors; 20 | 21 | import static org.mockito.Mockito.doAnswer; 22 | import static org.mockito.Mockito.doNothing; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.when; 25 | 26 | public class DeviceEmulator { 27 | 28 | private Device device; 29 | private DiscoveredDevice discoveredDevice; 30 | private Map characteristics = new HashMap<>(); 31 | private ScheduledFuture rssiFuture; 32 | private ArgumentCaptor rssiNotificationCaptor = ArgumentCaptor.forClass(Notification.class); 33 | private ArgumentCaptor connectionNotificationCaptor = ArgumentCaptor.forClass(Notification.class); 34 | private ArgumentCaptor servicesResolvedNotificationCaptor = ArgumentCaptor.forClass(Notification.class); 35 | 36 | public DeviceEmulator(URL url) { 37 | this(url, url.getDeviceAddress()); 38 | } 39 | 40 | public DeviceEmulator(URL url, String name) { 41 | device = mock(Device.class); 42 | discoveredDevice = mock(DiscoveredDevice.class); 43 | when(device.getURL()).thenReturn(url); 44 | when(device.getName()).thenReturn(name); 45 | 46 | when(device.getServices()).thenAnswer(answer -> 47 | characteristics.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getKey().getServiceURL())) 48 | .entrySet().stream().map(service -> new Service() { 49 | @Override 50 | public URL getURL() { 51 | return service.getKey(); 52 | } 53 | 54 | @Override 55 | public List getCharacteristics() { 56 | return service.getValue().stream().map(entry -> entry.getValue().getCharacteristic()).collect(Collectors.toList()); 57 | } 58 | }).collect(Collectors.toList())); 59 | 60 | discoveredDevice = new DiscoveredDevice(url, name, null, (short) 0, 1, true) { 61 | @Override 62 | public short getRSSI() { 63 | return device.getRSSI(); 64 | } 65 | }; 66 | 67 | doNothing().when(device).enableRSSINotifications(rssiNotificationCaptor.capture()); 68 | doNothing().when(device).enableConnectedNotifications(connectionNotificationCaptor.capture()); 69 | doNothing().when(device).enableServicesResolvedNotifications(servicesResolvedNotificationCaptor.capture()); 70 | 71 | doAnswer(answer -> { 72 | when(device.isConnected()).thenReturn(true); 73 | CompletableFuture.runAsync(() -> { 74 | connectionNotificationCaptor.getValue().notify(true); 75 | CompletableFuture.runAsync(() -> { 76 | when(device.isServicesResolved()).thenReturn(true); 77 | servicesResolvedNotificationCaptor.getValue().notify(true); 78 | }); 79 | }); 80 | return true; 81 | }).when(device).connect(); 82 | } 83 | 84 | public Device getDevice() { 85 | return device; 86 | } 87 | 88 | public Collection getCharacteristics() { 89 | return characteristics.values(); 90 | } 91 | 92 | public CharacteristicEmulator addCharacteristic(URL url, CharacteristicAccessType... flags) { 93 | CharacteristicEmulator characteristic = new CharacteristicEmulator(url, flags); 94 | characteristics.put(url, characteristic); 95 | return characteristic; 96 | } 97 | 98 | public void mockConnection(long delay, Runnable andThen) { 99 | doAnswer(answer -> { 100 | Thread.sleep(delay); 101 | when(device.isConnected()).thenReturn(true); 102 | CompletableFuture.runAsync(andThen); 103 | return true; 104 | }).when(device).connect(); 105 | } 106 | 107 | public void scheduleRandomRSSI(long delay) { 108 | if (rssiFuture != null) { 109 | rssiFuture.cancel(true); 110 | } 111 | 112 | rssiFuture = BluetoothFactoryEmulator.scheduler.scheduleWithFixedDelay((Runnable) () -> { 113 | short rssi = (short) -BluetoothFactoryEmulator.random.nextInt(100); 114 | when(device.getRSSI()).thenReturn(rssi); 115 | if (!rssiNotificationCaptor.getAllValues().isEmpty()) { 116 | rssiNotificationCaptor.getValue().notify(rssi); 117 | } 118 | }, 0, delay, TimeUnit.SECONDS); 119 | } 120 | 121 | public void cancelRSSI() { 122 | if (rssiFuture != null) { 123 | rssiFuture.cancel(true); 124 | rssiFuture = null; 125 | } 126 | } 127 | 128 | public DiscoveredDevice getDiscoveredDevice() { 129 | return discoveredDevice; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------