├── .gitignore ├── .idea ├── encodings.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── itan │ │ └── com │ │ └── bluetoothle │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── itan │ │ │ └── com │ │ │ └── bluetoothle │ │ │ ├── BluetoothActivity.java │ │ │ ├── CentralRoleActivity.java │ │ │ ├── CentralService.java │ │ │ ├── Constants.java │ │ │ ├── DeviceConnectActivity.java │ │ │ ├── DevicesAdapter.java │ │ │ ├── MainActivity.java │ │ │ ├── PeripheralAdvertiseService.java │ │ │ └── PeripheralRoleActivity.java │ └── res │ │ ├── drawable-hdpi │ │ └── ic_arrow_back_white_24dp.png │ │ ├── drawable-mdpi │ │ └── ic_arrow_back_white_24dp.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── ic_arrow_back_white_24dp.png │ │ ├── drawable-xxhdpi │ │ └── ic_arrow_back_white_24dp.png │ │ ├── drawable-xxxhdpi │ │ └── ic_arrow_back_white_24dp.png │ │ ├── drawable │ │ ├── color_option_1.xml │ │ ├── color_option_2.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_central_role.xml │ │ ├── activity_device_connect.xml │ │ ├── activity_main.xml │ │ ├── activity_peripheral_role.xml │ │ └── list_item_device_list.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── itan │ └── com │ └── bluetoothle │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── Screenshot_20180515-1521351_LI.jpg └── submarine_polygon_LI.jpg └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android Bluetooth Low Energy communication Sample 2 | =================================== 3 | Bla Bla 4 | 5 | Getting Started 6 | ------------ 7 | Bla Bla 8 | 9 | Screenshots 10 | ------------- 11 | Screenshot Screenshot 12 | Screenshot Screenshot 13 | 14 | Acknowledgements 15 | ------------ 16 | [android-BluetoothLeGatt][1] 17 | 18 | [android-BluetoothAdvertisements][2] 19 | 20 | [ble-test-peripheral-android][3] 21 | 22 | [1]:https://github.com/googlesamples/android-BluetoothLeGatt/ 23 | [2]:https://github.com/googlesamples/android-BluetoothAdvertisements/ 24 | [3]:https://github.com/WebBluetoothCG/ble-test-peripheral-android 25 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | defaultConfig { 6 | applicationId "itan.com.bluetoothle" 7 | /* 8 | In Android 5.0, an Android device can now act as a Bluetooth LE peripheral device. 9 | Android 4.3 introduced platform support for Bluetooth Low Energy (Bluetooth LE) 10 | in the central role 11 | */ 12 | minSdkVersion 21 13 | targetSdkVersion 26 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | compile fileTree(dir: 'libs', include: ['*.jar']) 28 | compile 'com.android.support:appcompat-v7:26.1.0' 29 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 30 | compile 'junit:junit:4.12' 31 | compile 'com.android.support.test:runner:1.0.1' 32 | compile 'com.android.support.test.espresso:espresso-core:3.0.1' 33 | compile 'com.android.support:design:26.1.0' 34 | } 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/itan/com/bluetoothle/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("itan.com.bluetoothle", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 44 | 45 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/BluetoothActivity.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothManager; 5 | import android.content.Context; 6 | import android.graphics.Color; 7 | import android.os.Bundle; 8 | import android.support.design.widget.Snackbar; 9 | import android.support.v7.app.ActionBar; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.View; 13 | 14 | /** 15 | * Created by itanbarpeled on 28/01/2018. 16 | */ 17 | 18 | public abstract class BluetoothActivity extends AppCompatActivity { 19 | 20 | 21 | private Toolbar mToolbar; 22 | 23 | 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(getLayoutId()); 29 | 30 | mToolbar = (Toolbar) findViewById(R.id.toolbar); 31 | 32 | setToolbar(); 33 | } 34 | 35 | 36 | protected abstract int getLayoutId(); 37 | 38 | protected abstract int getTitleString(); 39 | 40 | protected void onBackButtonClicked() { 41 | onBackPressed(); 42 | } 43 | 44 | protected void showMsgText(int stringId) { 45 | showMsgText(getString(stringId)); 46 | } 47 | 48 | protected void showMsgText(String string) { 49 | if (mToolbar != null) { 50 | Snackbar snackbar = Snackbar.make(mToolbar, string, Snackbar.LENGTH_LONG); 51 | snackbar.show(); 52 | } 53 | } 54 | 55 | protected BluetoothAdapter getBluetoothAdapter() { 56 | 57 | BluetoothAdapter bluetoothAdapter; 58 | BluetoothManager bluetoothService = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)); 59 | 60 | if (bluetoothService != null) { 61 | 62 | bluetoothAdapter = bluetoothService.getAdapter(); 63 | 64 | // Is Bluetooth supported on this device? 65 | if (bluetoothAdapter != null) { 66 | 67 | // Is Bluetooth turned on? 68 | if (bluetoothAdapter.isEnabled()) { 69 | /* 70 | all the other Bluetooth initial checks already verified in MainActivity 71 | */ 72 | return bluetoothAdapter; 73 | } 74 | } 75 | } 76 | 77 | return null; 78 | } 79 | 80 | 81 | private void setToolbar() { 82 | setSupportActionBar(mToolbar); 83 | 84 | ActionBar actionBar = getSupportActionBar(); 85 | if (actionBar != null) { 86 | actionBar.setDisplayShowTitleEnabled(false); 87 | actionBar.setDisplayHomeAsUpEnabled(true); 88 | actionBar.setDisplayShowHomeEnabled(true); 89 | } 90 | 91 | mToolbar.setNavigationOnClickListener(new View.OnClickListener() { 92 | @Override 93 | public void onClick(View view) { 94 | onBackButtonClicked(); 95 | } 96 | }); 97 | mToolbar.setTitle(getTitleString()); 98 | mToolbar.setTitleTextColor(Color.WHITE); 99 | } 100 | 101 | } 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/CentralRoleActivity.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.bluetooth.BluetoothAdapter; 4 | import android.bluetooth.BluetoothClass; 5 | import android.bluetooth.BluetoothDevice; 6 | import android.bluetooth.le.BluetoothLeScanner; 7 | import android.bluetooth.le.ScanCallback; 8 | import android.bluetooth.le.ScanFilter; 9 | import android.bluetooth.le.ScanResult; 10 | import android.bluetooth.le.ScanSettings; 11 | import android.content.Intent; 12 | import android.os.Bundle; 13 | import android.os.Handler; 14 | import android.os.Looper; 15 | import android.support.v7.widget.LinearLayoutManager; 16 | import android.support.v7.widget.RecyclerView; 17 | import android.util.Log; 18 | import android.view.View; 19 | import android.widget.Button; 20 | import android.widget.Toast; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | 27 | /** 28 | This activity represents the Central/Client role. 29 | Bluetooth communication flow: 30 | 1. advertise [peripheral] 31 | 2. scan [central] 32 | 3. connect [central] 33 | 4. notify [peripheral] 34 | 5. receive [central] 35 | */ 36 | public class CentralRoleActivity extends BluetoothActivity implements View.OnClickListener, DevicesAdapter.DevicesAdapterListener { 37 | 38 | 39 | /** 40 | * Stops scanning after 30 seconds. 41 | */ 42 | private static final long SCAN_PERIOD = 30000; 43 | 44 | private RecyclerView mDevicesRecycler; 45 | private DevicesAdapter mDevicesAdapter; 46 | private Button mScanButton; 47 | 48 | private ScanCallback mScanCallback; 49 | 50 | private Handler mHandler; 51 | 52 | 53 | @Override 54 | protected void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | 57 | mScanButton = (Button) findViewById(R.id.button_scan); 58 | mScanButton.setOnClickListener(this); 59 | 60 | mDevicesRecycler = (RecyclerView) findViewById(R.id.devices_recycler_view); 61 | mDevicesRecycler.setHasFixedSize(true); 62 | mDevicesRecycler.setLayoutManager(new LinearLayoutManager(this)); 63 | 64 | mDevicesAdapter = new DevicesAdapter(this); 65 | mDevicesRecycler.setAdapter(mDevicesAdapter); 66 | 67 | mHandler = new Handler(Looper.getMainLooper()); 68 | 69 | } 70 | 71 | 72 | @Override 73 | protected int getLayoutId() { 74 | return R.layout.activity_central_role; 75 | } 76 | 77 | 78 | @Override 79 | public void onClick(View view) { 80 | 81 | switch(view.getId()) { 82 | 83 | case R.id.button_scan: 84 | startBLEScan(); 85 | break; 86 | 87 | } 88 | } 89 | 90 | 91 | @Override 92 | protected int getTitleString() { 93 | return R.string.central_screen; 94 | } 95 | 96 | 97 | /* 98 | start Bluetooth Low Energy scan 99 | */ 100 | private void startBLEScan() { 101 | 102 | BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(); 103 | 104 | /* 105 | better to request each time as BluetoothAdapter state might change (connection lost, etc...) 106 | */ 107 | if (bluetoothAdapter != null) { 108 | 109 | BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); 110 | 111 | if (bluetoothLeScanner != null) { 112 | 113 | if (mScanCallback == null) { 114 | Log.d(MainActivity.TAG, "Starting Scanning"); 115 | 116 | // Will stop the scanning after a set time. 117 | mHandler.postDelayed(new Runnable() { 118 | @Override 119 | public void run() { 120 | stopScanning(); 121 | } 122 | }, SCAN_PERIOD); 123 | 124 | // Kick off a new scan. 125 | mScanCallback = new SampleScanCallback(); 126 | bluetoothLeScanner.startScan(buildScanFilters(), buildScanSettings(), mScanCallback); 127 | 128 | String toastText = 129 | getString(R.string.scan_start_toast) + " " 130 | + TimeUnit.SECONDS.convert(SCAN_PERIOD, TimeUnit.MILLISECONDS) + " " 131 | + getString(R.string.seconds); 132 | 133 | showMsgText(toastText); 134 | 135 | } else { 136 | showMsgText(R.string.already_scanning); 137 | } 138 | 139 | return; 140 | } 141 | } 142 | 143 | showMsgText(R.string.error_unknown); 144 | } 145 | 146 | /** 147 | * Return a List of {@link ScanFilter} objects to filter by Service UUID. 148 | */ 149 | private List buildScanFilters() { 150 | 151 | List scanFilters = new ArrayList<>(); 152 | 153 | ScanFilter.Builder builder = new ScanFilter.Builder(); 154 | // Comment out the below line to see all BLE devices around you 155 | //builder.setServiceUuid(Constants.SERVICE_UUID); 156 | scanFilters.add(builder.build()); 157 | 158 | return scanFilters; 159 | } 160 | 161 | /** 162 | * Return a {@link ScanSettings} object set to use low power (to preserve battery life). 163 | */ 164 | private ScanSettings buildScanSettings() { 165 | ScanSettings.Builder builder = new ScanSettings.Builder(); 166 | builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER); 167 | return builder.build(); 168 | } 169 | 170 | 171 | /** 172 | * Stop scanning for BLE Advertisements. 173 | */ 174 | public void stopScanning() { 175 | 176 | Log.d(MainActivity.TAG, "Stopping Scanning"); 177 | 178 | /* 179 | better to request each time as BluetoothAdapter state might change (connection lost, etc...) 180 | */ 181 | BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(); 182 | 183 | if (bluetoothAdapter != null) { 184 | 185 | BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); 186 | 187 | if (bluetoothLeScanner != null) { 188 | 189 | // Stop the scan, wipe the callback. 190 | bluetoothLeScanner.stopScan(mScanCallback); 191 | mScanCallback = null; 192 | 193 | // Even if no new results, update 'last seen' times. 194 | mDevicesAdapter.notifyDataSetChanged(); 195 | 196 | return; 197 | } 198 | } 199 | 200 | showMsgText(R.string.error_unknown); 201 | } 202 | 203 | 204 | @Override 205 | public void onDeviceItemClick(String deviceName, String deviceAddress) { 206 | 207 | //stopScanning(); 208 | 209 | Intent intent = new Intent(this, DeviceConnectActivity.class); 210 | intent.putExtra(DeviceConnectActivity.EXTRAS_DEVICE_NAME, deviceName); 211 | intent.putExtra(DeviceConnectActivity.EXTRAS_DEVICE_ADDRESS, deviceAddress); 212 | startActivity(intent); 213 | } 214 | 215 | 216 | /** 217 | * Custom ScanCallback object - adds to adapter on success, displays error on failure. 218 | */ 219 | private class SampleScanCallback extends ScanCallback { 220 | 221 | @Override 222 | public void onBatchScanResults(List results) { 223 | super.onBatchScanResults(results); 224 | mDevicesAdapter.add(results); 225 | logResults(results); 226 | } 227 | 228 | @Override 229 | public void onScanResult(int callbackType, ScanResult result) { 230 | super.onScanResult(callbackType, result); 231 | mDevicesAdapter.add(result); 232 | logResults(result); 233 | } 234 | 235 | @Override 236 | public void onScanFailed(int errorCode) { 237 | super.onScanFailed(errorCode); 238 | showMsgText("Scan failed with error: " + errorCode); 239 | } 240 | 241 | 242 | private void logResults(List results) { 243 | if (results != null) { 244 | for (ScanResult result : results) { 245 | logResults(result); 246 | } 247 | } 248 | } 249 | 250 | private void logResults(ScanResult result) { 251 | if (result != null) { 252 | BluetoothDevice device = result.getDevice(); 253 | if (device != null) { 254 | Log.v(MainActivity.TAG, device.getName() + " " + device.getAddress()); 255 | return; 256 | } 257 | } 258 | Log.e(MainActivity.TAG, "error SampleScanCallback"); 259 | } 260 | } 261 | 262 | 263 | } 264 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/CentralService.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.app.Service; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothDevice; 6 | import android.bluetooth.BluetoothGatt; 7 | import android.bluetooth.BluetoothGattCallback; 8 | import android.bluetooth.BluetoothGattCharacteristic; 9 | import android.bluetooth.BluetoothGattDescriptor; 10 | import android.bluetooth.BluetoothGattService; 11 | import android.bluetooth.BluetoothManager; 12 | import android.bluetooth.BluetoothProfile; 13 | import android.content.Context; 14 | import android.content.Intent; 15 | import android.os.Binder; 16 | import android.os.IBinder; 17 | import android.util.Log; 18 | import java.util.List; 19 | 20 | import static itan.com.bluetoothle.Constants.BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID; 21 | import static itan.com.bluetoothle.Constants.HEART_RATE_SERVICE_UUID; 22 | 23 | /** 24 | * Created by itanbarpeled on 28/01/2018. 25 | */ 26 | 27 | public class CentralService extends Service { 28 | 29 | private static final int STATE_DISCONNECTED = 0; 30 | private static final int STATE_CONNECTING = 1; 31 | private static final int STATE_CONNECTED = 2; 32 | 33 | public final static String ACTION_GATT_CONNECTED = "ACTION_GATT_CONNECTED"; 34 | public final static String ACTION_GATT_DISCONNECTED = "ACTION_GATT_DISCONNECTED"; 35 | public final static String ACTION_GATT_SERVICES_DISCOVERED = "ACTION_GATT_SERVICES_DISCOVERED"; 36 | public final static String ACTION_DATA_AVAILABLE = "ACTION_DATA_AVAILABLE"; 37 | public final static String EXTRA_DATA = "EXTRA_DATA"; 38 | 39 | 40 | private BluetoothManager mBluetoothManager; 41 | private BluetoothAdapter mBluetoothAdapter; 42 | private String mBluetoothDeviceAddress; 43 | private BluetoothGatt mBluetoothGatt; 44 | 45 | private final IBinder mBinder = new LocalBinder(); 46 | private int mConnectionState = STATE_DISCONNECTED; 47 | 48 | 49 | /* 50 | Implements callback methods for GATT events that the app cares about. For example, 51 | connection change and services discovered. 52 | */ 53 | private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { 54 | 55 | @Override 56 | public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 57 | 58 | String intentAction; 59 | 60 | if (newState == BluetoothProfile.STATE_CONNECTED) { 61 | intentAction = ACTION_GATT_CONNECTED; 62 | mConnectionState = STATE_CONNECTED; 63 | broadcastUpdate(intentAction); 64 | Log.i(MainActivity.TAG, "Connected to GATT server."); 65 | // Attempts to discover services after successful connection. 66 | Log.i(MainActivity.TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices()); 67 | 68 | } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { 69 | intentAction = ACTION_GATT_DISCONNECTED; 70 | mConnectionState = STATE_DISCONNECTED; 71 | Log.i(MainActivity.TAG, "Disconnected from GATT server."); 72 | broadcastUpdate(intentAction); 73 | } 74 | } 75 | 76 | @Override 77 | public void onServicesDiscovered(BluetoothGatt gatt, int status) { 78 | if (status == BluetoothGatt.GATT_SUCCESS) { 79 | broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); 80 | } else { 81 | Log.w(MainActivity.TAG, "onServicesDiscovered received: " + status); 82 | } 83 | } 84 | 85 | @Override 86 | public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 87 | if (status == BluetoothGatt.GATT_SUCCESS) { 88 | broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); 89 | } else { 90 | Log.w(MainActivity.TAG, "onCharacteristicRead GATT_FAILURE"); 91 | } 92 | } 93 | 94 | @Override 95 | public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { 96 | broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); 97 | } 98 | }; 99 | 100 | 101 | private void broadcastUpdate(final String action) { 102 | final Intent intent = new Intent(action); 103 | sendBroadcast(intent); 104 | } 105 | 106 | 107 | private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) { 108 | 109 | Intent intent = new Intent(action); 110 | 111 | /* 112 | This is special handling for the Heart Rate Measurement profile. Data parsing is 113 | carried out as per profile specifications: 114 | http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml 115 | */ 116 | if (BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) { 117 | 118 | int flag = characteristic.getProperties(); 119 | int format = -1; 120 | 121 | if ((flag & 0x01) != 0) { 122 | format = BluetoothGattCharacteristic.FORMAT_UINT16; 123 | Log.d(MainActivity.TAG, "data format UINT16."); 124 | } else { 125 | format = BluetoothGattCharacteristic.FORMAT_UINT8; 126 | Log.d(MainActivity.TAG, "data format UINT16."); 127 | } 128 | 129 | int msg = characteristic.getIntValue(format, 0); 130 | Log.d(MainActivity.TAG, String.format("message: %d", msg)); 131 | intent.putExtra(EXTRA_DATA, msg); 132 | 133 | } else { 134 | 135 | /* 136 | for all other profiles, writes the data formatted in HEX. 137 | this code isn't relevant for this project. 138 | */ 139 | final byte[] data = characteristic.getValue(); 140 | 141 | if (data != null && data.length > 0) { 142 | final StringBuilder stringBuilder = new StringBuilder(data.length); 143 | for (byte byteChar : data) { 144 | stringBuilder.append(String.format("%02X ", byteChar)); 145 | } 146 | 147 | Log.w(MainActivity.TAG, "broadcastUpdate. general profile"); 148 | intent.putExtra(EXTRA_DATA, -1); 149 | //intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); 150 | } 151 | } 152 | 153 | 154 | sendBroadcast(intent); 155 | } 156 | 157 | public class LocalBinder extends Binder { 158 | CentralService getService() { 159 | return CentralService.this; 160 | } 161 | } 162 | 163 | @Override 164 | public IBinder onBind(Intent intent) { 165 | return mBinder; 166 | } 167 | 168 | @Override 169 | public boolean onUnbind(Intent intent) { 170 | // After using a given device, you should make sure that BluetoothGatt.close() is called 171 | // such that resources are cleaned up properly. In this particular example, close() is 172 | // invoked when the UI is disconnected from the Service. 173 | close(); 174 | return super.onUnbind(intent); 175 | } 176 | 177 | 178 | /** 179 | * Initializes a reference to the local Bluetooth adapter. 180 | * 181 | * @return Return true if the initialization is successful. 182 | */ 183 | public boolean initialize() { 184 | 185 | // For API level 18 and above, get a reference to BluetoothAdapter through BluetoothManager. 186 | if (mBluetoothManager == null) { 187 | 188 | mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 189 | 190 | if (mBluetoothManager == null) { 191 | Log.e(MainActivity.TAG, "Unable to initialize BluetoothManager."); 192 | return false; 193 | } 194 | } 195 | 196 | mBluetoothAdapter = mBluetoothManager.getAdapter(); 197 | 198 | if (mBluetoothAdapter == null) { 199 | Log.e(MainActivity.TAG, "Unable to obtain a BluetoothAdapter."); 200 | return false; 201 | } 202 | 203 | return true; 204 | } 205 | 206 | /** 207 | * Connects to the GATT server hosted on the Bluetooth LE device. 208 | * 209 | * @param address The device address of the destination device. 210 | * 211 | * @return Return true if the connection is initiated successfully. The connection result 212 | * is reported asynchronously through the 213 | * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} 214 | * callback. 215 | */ 216 | public boolean connect(final String address) { 217 | 218 | if (mBluetoothAdapter == null || address == null) { 219 | Log.w(MainActivity.TAG, "BluetoothAdapter not initialized or unspecified address."); 220 | return false; 221 | } 222 | 223 | // Previously connected device. Try to reconnect. 224 | if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) { 225 | Log.d(MainActivity.TAG, "Trying to use an existing mBluetoothGatt for connection."); 226 | if (mBluetoothGatt.connect()) { 227 | mConnectionState = STATE_CONNECTING; 228 | return true; 229 | } else { 230 | return false; 231 | } 232 | } 233 | 234 | BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); 235 | if (device == null) { 236 | Log.w(MainActivity.TAG, "Device not found. Unable to connect."); 237 | return false; 238 | } 239 | 240 | // We want to directly connect to the device, so we are setting the autoConnect 241 | // parameter to false. 242 | mBluetoothGatt = device.connectGatt(this, false, mGattCallback); 243 | mBluetoothDeviceAddress = address; 244 | mConnectionState = STATE_CONNECTING; 245 | 246 | Log.d(MainActivity.TAG, "Trying to create a new connection."); 247 | 248 | return true; 249 | } 250 | 251 | 252 | // TODO bluetooth - call this method when needed 253 | /** 254 | * Disconnects an existing connection or cancel a pending connection. The disconnection result 255 | * is reported asynchronously through the 256 | * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} 257 | * callback. 258 | */ 259 | public void disconnect() { 260 | 261 | if (mBluetoothAdapter == null || mBluetoothGatt == null) { 262 | Log.w(MainActivity.TAG, "BluetoothAdapter not initialized"); 263 | return; 264 | } 265 | 266 | mBluetoothGatt.disconnect(); 267 | } 268 | 269 | /** 270 | * After using a given BLE device, the app must call this method to ensure resources are 271 | * released properly. 272 | */ 273 | public void close() { 274 | 275 | if (mBluetoothGatt == null) { 276 | return; 277 | } 278 | 279 | mBluetoothGatt.close(); 280 | mBluetoothGatt = null; 281 | } 282 | 283 | 284 | 285 | /** 286 | * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported 287 | * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} 288 | * callback. 289 | * 290 | * @param characteristic The characteristic to read from. 291 | */ 292 | public void readCharacteristic(BluetoothGattCharacteristic characteristic) { 293 | 294 | if (mBluetoothAdapter == null || mBluetoothGatt == null) { 295 | Log.w(MainActivity.TAG, "BluetoothAdapter not initialized"); 296 | return; 297 | } 298 | 299 | mBluetoothGatt.readCharacteristic(characteristic); 300 | } 301 | 302 | /** 303 | * Enables or disables notification on a give characteristic. 304 | * 305 | * @param characteristic Characteristic to act on. 306 | * @param enabled If true, enable notification. False otherwise. 307 | */ 308 | public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) { 309 | 310 | if (mBluetoothAdapter == null || mBluetoothGatt == null) { 311 | Log.w(MainActivity.TAG, "BluetoothAdapter not initialized"); 312 | return; 313 | } 314 | 315 | mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); 316 | 317 | // This is specific to Heart Rate Measurement. 318 | /* 319 | if (HEART_RATE_MEASUREMENT_UUID.toString().equals(characteristic.getUuid().toString())) { 320 | BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID); 321 | descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); 322 | mBluetoothGatt.writeDescriptor(descriptor); 323 | } 324 | */ 325 | 326 | } 327 | 328 | /** 329 | * Retrieves a list of supported GATT services on the connected device. This should be 330 | * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. 331 | * 332 | * @return A {@code List} of supported services. 333 | */ 334 | public List getSupportedGattServices() { 335 | if (mBluetoothGatt == null) { 336 | return null; 337 | } 338 | 339 | return mBluetoothGatt.getServices(); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/Constants.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic; 4 | import android.os.ParcelUuid; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Created by itanbarpeled on 28/01/2018. 10 | */ 11 | 12 | public class Constants { 13 | 14 | 15 | public static final int SERVER_MSG_FIRST_STATE = 1; 16 | public static final int SERVER_MSG_SECOND_STATE = 2; 17 | 18 | /* 19 | TODO bluetooth 20 | better to use different Bluetooth Service, 21 | instead of Heart Rate Service: 22 | https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml. 23 | 24 | maybe Object Transfer Service is more suitable: 25 | https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.object_transfer.xml 26 | */ 27 | public static final UUID HEART_RATE_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb"); 28 | public static final UUID BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb"); 29 | 30 | 31 | 32 | 33 | private static UUID convertFromInteger(int i) { 34 | final long MSB = 0x0000000000001000L; 35 | final long LSB = 0x800000805f9b34fbL; 36 | long value = i & 0xFFFFFFFF; 37 | return new UUID(MSB | (value << 32), LSB); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/DeviceConnectActivity.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.bluetooth.BluetoothGattCharacteristic; 4 | import android.bluetooth.BluetoothGattService; 5 | import android.content.BroadcastReceiver; 6 | import android.content.ComponentName; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.IntentFilter; 10 | import android.content.ServiceConnection; 11 | import android.graphics.Color; 12 | import android.os.IBinder; 13 | import android.os.Bundle; 14 | import android.text.TextUtils; 15 | import android.util.Log; 16 | import android.view.View; 17 | import android.widget.Button; 18 | import android.widget.ImageView; 19 | import android.widget.TextView; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import static itan.com.bluetoothle.Constants.BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID; 23 | import static itan.com.bluetoothle.Constants.HEART_RATE_SERVICE_UUID; 24 | import static itan.com.bluetoothle.Constants.SERVER_MSG_FIRST_STATE; 25 | import static itan.com.bluetoothle.Constants.SERVER_MSG_SECOND_STATE; 26 | 27 | 28 | 29 | public class DeviceConnectActivity extends BluetoothActivity implements View.OnClickListener { 30 | 31 | public static final String EXTRAS_DEVICE_NAME = "DEVICE_NAME"; 32 | public static final String EXTRAS_DEVICE_ADDRESS = "DEVICE_ADDRESS"; 33 | private static final String LIST_NAME = "NAME"; 34 | private static final String LIST_UUID = "UUID"; 35 | 36 | 37 | private CentralService mBluetoothLeService; 38 | private ArrayList> mDeviceServices; 39 | private BluetoothGattCharacteristic mCharacteristic; 40 | 41 | private String mDeviceName; 42 | private String mDeviceAddress; 43 | 44 | private TextView mConnectionStatus; 45 | private TextView mConnectedDeviceName; 46 | private ImageView mServerCharacteristic; 47 | private Button mRequestReadCharacteristic; 48 | 49 | 50 | @Override 51 | protected void onCreate(Bundle savedInstanceState) { 52 | super.onCreate(savedInstanceState); 53 | 54 | mDeviceServices = new ArrayList<>(); 55 | mCharacteristic = null; 56 | 57 | Intent intent = getIntent(); 58 | if (intent != null) { 59 | mDeviceName = intent.getStringExtra(EXTRAS_DEVICE_NAME); 60 | mDeviceAddress = intent.getStringExtra(EXTRAS_DEVICE_ADDRESS); 61 | } 62 | 63 | 64 | mConnectionStatus = (TextView) findViewById(R.id.connection_status); 65 | mConnectedDeviceName = (TextView) findViewById(R.id.connected_device_name); 66 | mServerCharacteristic = (ImageView) findViewById(R.id.server_characteristic_value); 67 | mRequestReadCharacteristic = (Button) findViewById(R.id.request_read_characteristic); 68 | mRequestReadCharacteristic.setOnClickListener(this); 69 | 70 | 71 | if (TextUtils.isEmpty(mDeviceName)) { 72 | mConnectedDeviceName.setText(""); 73 | } else { 74 | mConnectedDeviceName.setText(mDeviceName); 75 | } 76 | 77 | 78 | Intent gattServiceIntent = new Intent(this, CentralService.class); 79 | bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE); 80 | 81 | /* 82 | updateConnectionState(R.string.connected); 83 | mRequestReadCharacteristic.setEnabled(true); 84 | updateInputFromServer(SERVER_MSG_SECOND_STATE); 85 | */ 86 | } 87 | 88 | 89 | @Override 90 | protected void onResume() { 91 | super.onResume(); 92 | registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter()); 93 | } 94 | 95 | 96 | @Override 97 | protected void onPause() { 98 | super.onPause(); 99 | unregisterReceiver(mGattUpdateReceiver); 100 | } 101 | 102 | 103 | @Override 104 | protected void onDestroy() { 105 | super.onDestroy(); 106 | unbindService(mServiceConnection); 107 | mBluetoothLeService = null; 108 | } 109 | 110 | @Override 111 | protected int getLayoutId() { 112 | return R.layout.activity_device_connect; 113 | } 114 | 115 | @Override 116 | protected int getTitleString() { 117 | return R.string.central_connection_screen; 118 | } 119 | 120 | 121 | @Override 122 | public void onClick(View view) { 123 | 124 | switch (view.getId()) { 125 | 126 | case R.id.request_read_characteristic: 127 | requestReadCharacteristic(); 128 | break; 129 | 130 | } 131 | } 132 | 133 | 134 | /* 135 | request from the Server the value of the Characteristic. 136 | this request is asynchronous. 137 | */ 138 | private void requestReadCharacteristic() { 139 | if (mBluetoothLeService != null && mCharacteristic != null) { 140 | mBluetoothLeService.readCharacteristic(mCharacteristic); 141 | } else { 142 | showMsgText(R.string.error_unknown); 143 | } 144 | } 145 | 146 | 147 | // Code to manage Service lifecycle. 148 | private final ServiceConnection mServiceConnection = new ServiceConnection() { 149 | 150 | @Override 151 | public void onServiceConnected(ComponentName componentName, IBinder service) { 152 | 153 | mBluetoothLeService = ((CentralService.LocalBinder) service).getService(); 154 | 155 | if (!mBluetoothLeService.initialize()) { 156 | Log.e(MainActivity.TAG, "Unable to initialize Bluetooth"); 157 | finish(); 158 | } 159 | 160 | // Automatically connects to the device upon successful start-up initialization. 161 | mBluetoothLeService.connect(mDeviceAddress); 162 | } 163 | 164 | @Override 165 | public void onServiceDisconnected(ComponentName componentName) { 166 | mBluetoothLeService = null; 167 | } 168 | }; 169 | 170 | 171 | /* 172 | Handles various events fired by the Service. 173 | ACTION_GATT_CONNECTED: connected to a GATT server. 174 | ACTION_GATT_DISCONNECTED: disconnected from a GATT server. 175 | ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services. 176 | ACTION_DATA_AVAILABLE: received data from the device. This can be a result of read or notification operations. 177 | */ 178 | private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() { 179 | @Override 180 | public void onReceive(Context context, Intent intent) { 181 | 182 | String action = intent.getAction(); 183 | 184 | if (action == null) { 185 | return; 186 | } 187 | 188 | switch (intent.getAction()) { 189 | 190 | case CentralService.ACTION_GATT_CONNECTED: 191 | updateConnectionState(R.string.connected); 192 | mRequestReadCharacteristic.setEnabled(true); 193 | break; 194 | 195 | case CentralService.ACTION_GATT_DISCONNECTED: 196 | updateConnectionState(R.string.disconnected); 197 | mRequestReadCharacteristic.setEnabled(false); 198 | break; 199 | 200 | 201 | case CentralService.ACTION_GATT_SERVICES_DISCOVERED: 202 | // set all the supported services and characteristics on the user interface. 203 | setGattServices(mBluetoothLeService.getSupportedGattServices()); 204 | registerCharacteristic(); 205 | break; 206 | 207 | case CentralService.ACTION_DATA_AVAILABLE: 208 | int msg = intent.getIntExtra(CentralService.EXTRA_DATA, -1); 209 | Log.v(MainActivity.TAG, "ACTION_DATA_AVAILABLE " + msg); 210 | updateInputFromServer(msg); 211 | break; 212 | 213 | } 214 | } 215 | }; 216 | 217 | 218 | /* 219 | This sample demonstrates 'Read' and 'Notify' features. 220 | See http://d.android.com/reference/android/bluetooth/BluetoothGatt.html for the complete 221 | list of supported characteristic features. 222 | */ 223 | private void registerCharacteristic() { 224 | 225 | BluetoothGattCharacteristic characteristic = null; 226 | 227 | if (mDeviceServices != null) { 228 | 229 | /* iterate all the Services the connected device offer. 230 | a Service is a collection of Characteristic. 231 | */ 232 | for (ArrayList service : mDeviceServices) { 233 | 234 | // iterate all the Characteristic of the Service 235 | for (BluetoothGattCharacteristic serviceCharacteristic : service) { 236 | 237 | /* check this characteristic belongs to the Service defined in 238 | PeripheralAdvertiseService.buildAdvertiseData() method 239 | */ 240 | if (serviceCharacteristic.getService().getUuid().equals(HEART_RATE_SERVICE_UUID)) { 241 | 242 | if (serviceCharacteristic.getUuid().equals(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID)) { 243 | characteristic = serviceCharacteristic; 244 | mCharacteristic = characteristic; 245 | } 246 | } 247 | } 248 | } 249 | 250 | /* 251 | int charaProp = characteristic.getProperties(); 252 | if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) { 253 | */ 254 | 255 | if (characteristic != null) { 256 | mBluetoothLeService.readCharacteristic(characteristic); 257 | mBluetoothLeService.setCharacteristicNotification(characteristic, true); 258 | } 259 | } 260 | } 261 | 262 | 263 | /* 264 | Demonstrates how to iterate through the supported GATT Services/Characteristics. 265 | */ 266 | private void setGattServices(List gattServices) { 267 | 268 | if (gattServices == null) { 269 | return; 270 | } 271 | 272 | mDeviceServices = new ArrayList<>(); 273 | 274 | // Loops through available GATT Services from the connected device 275 | for (BluetoothGattService gattService : gattServices) { 276 | ArrayList characteristic = new ArrayList<>(); 277 | characteristic.addAll(gattService.getCharacteristics()); // each GATT Service can have multiple characteristic 278 | mDeviceServices.add(characteristic); 279 | } 280 | 281 | } 282 | 283 | 284 | private void updateConnectionState(final int resourceId) { 285 | runOnUiThread(new Runnable() { 286 | @Override 287 | public void run() { 288 | mConnectionStatus.setText(resourceId); 289 | } 290 | }); 291 | } 292 | 293 | 294 | private static IntentFilter makeGattUpdateIntentFilter() { 295 | final IntentFilter intentFilter = new IntentFilter(); 296 | intentFilter.addAction(CentralService.ACTION_GATT_CONNECTED); 297 | intentFilter.addAction(CentralService.ACTION_GATT_DISCONNECTED); 298 | intentFilter.addAction(CentralService.ACTION_GATT_SERVICES_DISCOVERED); 299 | intentFilter.addAction(CentralService.ACTION_DATA_AVAILABLE); 300 | return intentFilter; 301 | } 302 | 303 | 304 | private void updateInputFromServer(int msg) { 305 | 306 | String color; 307 | 308 | switch (msg) { 309 | case SERVER_MSG_FIRST_STATE: 310 | color = "#AD1457"; 311 | break; 312 | 313 | case SERVER_MSG_SECOND_STATE: 314 | color = "#6A1B9A"; 315 | break; 316 | 317 | default: 318 | color = "#FFFFFF"; 319 | break; 320 | 321 | } 322 | 323 | mServerCharacteristic.setBackgroundColor(Color.parseColor(color)); 324 | showMsgText(String.format(getString(R.string.characteristic_value_received), msg)); 325 | } 326 | 327 | 328 | 329 | 330 | 331 | 332 | } 333 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/DevicesAdapter.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.bluetooth.BluetoothDevice; 4 | import android.bluetooth.le.ScanResult; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.text.TextUtils; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.TextView; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by itanbarpeled on 28/01/2018. 17 | */ 18 | 19 | public class DevicesAdapter extends RecyclerView.Adapter { 20 | 21 | 22 | public interface DevicesAdapterListener { 23 | void onDeviceItemClick(String deviceName, String deviceAddress); 24 | } 25 | 26 | /* 27 | class Temp { 28 | String name; 29 | String address; 30 | 31 | public Temp(String name, String address) { 32 | this.name = name; 33 | this.address = address; 34 | } 35 | } 36 | */ 37 | 38 | static class ViewHolder extends RecyclerView.ViewHolder { 39 | 40 | TextView mDeviceNameView; 41 | TextView mDeviceNameAddressView; 42 | 43 | ViewHolder(View view) { 44 | 45 | super(view); 46 | mDeviceNameView = (TextView) view.findViewById(R.id.device_name); 47 | mDeviceNameAddressView = (TextView) view.findViewById(R.id.device_address); 48 | } 49 | 50 | } 51 | 52 | //private ArrayList mArrayList; 53 | private ArrayList mArrayList; 54 | private DevicesAdapterListener mListener; 55 | 56 | 57 | public DevicesAdapter(DevicesAdapterListener listener) { 58 | mArrayList = new ArrayList<>(); 59 | mListener = listener; 60 | 61 | /* 62 | Temp device = new Temp("Pixel", "AA:12:AA:12:AA:12"); 63 | mArrayList.add(device); 64 | device = new Temp("Galaxy S8", "BB:23:BB:23:BB:23"); 65 | mArrayList.add(device); 66 | notifyDataSetChanged(); 67 | */ 68 | } 69 | 70 | public DevicesAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 71 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_device_list, parent, false); 72 | return new ViewHolder(view); 73 | } 74 | 75 | @Override 76 | public void onBindViewHolder(ViewHolder holder, int position) { 77 | 78 | ScanResult scanResult = mArrayList.get(position); 79 | final String deviceName = scanResult.getDevice().getName(); 80 | final String deviceAddress = scanResult.getDevice().getAddress(); 81 | 82 | /* 83 | Temp scanResult = mArrayList.get(position); 84 | final String deviceName = scanResult.name; 85 | final String deviceAddress = scanResult.address; 86 | */ 87 | 88 | if (TextUtils.isEmpty(deviceName)) { 89 | holder.mDeviceNameView.setText(""); 90 | } else { 91 | holder.mDeviceNameView.setText(deviceName); 92 | } 93 | 94 | if (TextUtils.isEmpty(deviceAddress)) { 95 | holder.mDeviceNameAddressView.setText(""); 96 | } else { 97 | holder.mDeviceNameAddressView.setText(deviceAddress); 98 | } 99 | 100 | holder.itemView.setOnClickListener(new View.OnClickListener() { 101 | @Override 102 | public void onClick(View view) { 103 | if (!TextUtils.isEmpty(deviceName) && !TextUtils.isEmpty(deviceAddress)) { 104 | if (mListener != null) { 105 | mListener.onDeviceItemClick(deviceName, deviceAddress); 106 | } 107 | } 108 | } 109 | }); 110 | } 111 | 112 | @Override 113 | public int getItemCount() { 114 | return mArrayList.size(); 115 | } 116 | 117 | public void add(ScanResult scanResult) { 118 | add(scanResult, true); 119 | } 120 | 121 | /** 122 | * Add a ScanResult item to the adapter if a result from that device isn't already present. 123 | * Otherwise updates the existing position with the new ScanResult. 124 | */ 125 | public void add(ScanResult scanResult, boolean notify) { 126 | 127 | if (scanResult == null) { 128 | return; 129 | } 130 | 131 | int existingPosition = getPosition(scanResult.getDevice().getAddress()); 132 | 133 | if (existingPosition >= 0) { 134 | // Device is already in list, update its record. 135 | mArrayList.set(existingPosition, scanResult); 136 | } else { 137 | // Add new Device's ScanResult to list. 138 | mArrayList.add(scanResult); 139 | } 140 | 141 | if (notify) { 142 | notifyDataSetChanged(); 143 | } 144 | 145 | } 146 | 147 | public void add(List scanResults) { 148 | if (scanResults != null) { 149 | for (ScanResult scanResult : scanResults) { 150 | add(scanResult, false); 151 | } 152 | notifyDataSetChanged(); 153 | } 154 | } 155 | 156 | 157 | /** 158 | * Search the adapter for an existing device address and return it, otherwise return -1. 159 | */ 160 | private int getPosition(String address) { 161 | int position = -1; 162 | for (int i = 0; i < mArrayList.size(); i++) { 163 | if (mArrayList.get(i).getDevice().getAddress().equals(address)) { 164 | position = i; 165 | break; 166 | } 167 | } 168 | return position; 169 | } 170 | 171 | 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/MainActivity.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.Manifest; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothManager; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.os.Build; 10 | import android.support.annotation.NonNull; 11 | import android.support.design.widget.Snackbar; 12 | import android.support.v4.app.ActivityCompat; 13 | import android.support.v7.app.AppCompatActivity; 14 | import android.os.Bundle; 15 | import android.view.View; 16 | import android.widget.Button; 17 | import android.widget.Toast; 18 | 19 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 20 | 21 | public static final String TAG = "BluetoothLE"; 22 | private static final int REQUEST_ENABLE_BT = 1; 23 | private static final int PERMISSION_REQUEST_COARSE_LOCATION = 2; 24 | 25 | 26 | private Button mPeripheralButton; 27 | private Button mCentralButton; 28 | 29 | private BluetoothAdapter mBluetoothAdapter; 30 | 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main); 36 | 37 | mPeripheralButton = (Button) findViewById(R.id.button_role_peripheral); 38 | mCentralButton = (Button) findViewById(R.id.button_role_central); 39 | 40 | mPeripheralButton.setOnClickListener(this); 41 | mCentralButton.setOnClickListener(this); 42 | 43 | if (savedInstanceState == null) { 44 | initBT(); 45 | } 46 | 47 | } 48 | 49 | 50 | @Override 51 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 52 | 53 | super.onActivityResult(requestCode, resultCode, data); 54 | 55 | switch (requestCode) { 56 | case REQUEST_ENABLE_BT: 57 | 58 | if (resultCode == RESULT_OK) { 59 | 60 | initBT(); 61 | 62 | } else { 63 | 64 | // User declined to enable Bluetooth, exit the app. 65 | Toast.makeText(this, R.string.bt_not_enabled, Toast.LENGTH_SHORT).show(); 66 | } 67 | break; 68 | 69 | default: 70 | super.onActivityResult(requestCode, resultCode, data); 71 | break; 72 | } 73 | } 74 | 75 | 76 | @Override 77 | public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { 78 | 79 | switch (requestCode) { 80 | 81 | case PERMISSION_REQUEST_COARSE_LOCATION: 82 | if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { 83 | showErrorText(R.string.bt_not_permit_coarse); 84 | } else { 85 | // Everything is supported and enabled. 86 | enableNavigation(); 87 | } 88 | break; 89 | 90 | } 91 | } 92 | 93 | 94 | private void initBT() { 95 | 96 | BluetoothManager bluetoothService = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)); 97 | 98 | if (bluetoothService != null) { 99 | 100 | mBluetoothAdapter = bluetoothService.getAdapter(); 101 | 102 | // Is Bluetooth supported on this device? 103 | if (mBluetoothAdapter != null) { 104 | 105 | // Is Bluetooth turned on? 106 | if (mBluetoothAdapter.isEnabled()) { 107 | 108 | // Are Bluetooth Advertisements supported on this device? 109 | if (mBluetoothAdapter.isMultipleAdvertisementSupported()) { 110 | 111 | // see https://stackoverflow.com/a/37015725/1869297 112 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 113 | 114 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { 115 | requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSION_REQUEST_COARSE_LOCATION); 116 | } else { 117 | // Everything is supported and enabled. 118 | enableNavigation(); 119 | } 120 | 121 | } else { 122 | // Everything is supported and enabled. 123 | enableNavigation(); 124 | } 125 | 126 | 127 | } else { 128 | 129 | // Bluetooth Advertisements are not supported. 130 | showErrorText(R.string.bt_ads_not_supported); 131 | } 132 | } else { 133 | 134 | // Prompt user to turn on Bluetooth (logic continues in onActivityResult()). 135 | Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); 136 | startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); 137 | } 138 | } else { 139 | 140 | // Bluetooth is not supported. 141 | showErrorText(R.string.bt_not_supported); 142 | } 143 | 144 | } 145 | } 146 | 147 | 148 | @Override 149 | public void onClick(View view) { 150 | 151 | Intent intent = null; 152 | 153 | switch(view.getId()) { 154 | 155 | case R.id.button_role_peripheral: 156 | intent = new Intent(this, PeripheralRoleActivity.class); 157 | break; 158 | 159 | case R.id.button_role_central: 160 | intent = new Intent(this, CentralRoleActivity.class); 161 | break; 162 | 163 | } 164 | 165 | if (intent != null) { 166 | startActivity(intent); 167 | } 168 | } 169 | 170 | 171 | private void enableNavigation() { 172 | mPeripheralButton.setEnabled(true); 173 | mCentralButton.setEnabled(true); 174 | } 175 | 176 | 177 | private void showErrorText(int string) { 178 | Snackbar snackbar = Snackbar.make(mPeripheralButton, string, Snackbar.LENGTH_LONG); 179 | snackbar.show(); 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/PeripheralAdvertiseService.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.app.Service; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothManager; 6 | import android.bluetooth.le.AdvertiseCallback; 7 | import android.bluetooth.le.AdvertiseData; 8 | import android.bluetooth.le.AdvertiseSettings; 9 | import android.bluetooth.le.BluetoothLeAdvertiser; 10 | import android.content.Context; 11 | import android.content.Intent; 12 | import android.os.Handler; 13 | import android.os.IBinder; 14 | import android.os.ParcelUuid; 15 | import android.util.Log; 16 | 17 | import java.util.concurrent.TimeUnit; 18 | 19 | /** 20 | * Created by itanbarpeled on 28/01/2018. 21 | */ 22 | 23 | public class PeripheralAdvertiseService extends Service { 24 | 25 | 26 | /** 27 | * A global variable to let AdvertiserFragment check if the Service is running without needing 28 | * to start or bind to it. 29 | * This is the best practice method as defined here: 30 | * https://groups.google.com/forum/#!topic/android-developers/jEvXMWgbgzE 31 | */ 32 | public static boolean running = false; 33 | 34 | private BluetoothLeAdvertiser mBluetoothLeAdvertiser; 35 | private AdvertiseCallback mAdvertiseCallback; 36 | private Handler mHandler; 37 | private Runnable timeoutRunnable; 38 | 39 | /** 40 | * Length of time to allow advertising before automatically shutting off. (10 minutes) 41 | */ 42 | private long TIMEOUT = TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES); 43 | 44 | @Override 45 | public void onCreate() { 46 | running = true; 47 | initialize(); 48 | startAdvertising(); 49 | setTimeout(); 50 | super.onCreate(); 51 | } 52 | 53 | @Override 54 | public void onDestroy() { 55 | /** 56 | * Note that onDestroy is not guaranteed to be called quickly or at all. Services exist at 57 | * the whim of the system, and onDestroy can be delayed or skipped entirely if memory need 58 | * is critical. 59 | */ 60 | running = false; 61 | stopAdvertising(); 62 | mHandler.removeCallbacks(timeoutRunnable); 63 | stopForeground(true); 64 | super.onDestroy(); 65 | } 66 | 67 | /** 68 | * Required for extending service, but this will be a Started Service only, so no need for 69 | * binding. 70 | */ 71 | @Override 72 | public IBinder onBind(Intent intent) { 73 | return null; 74 | } 75 | 76 | /** 77 | * Get references to system Bluetooth objects if we don't have them already. 78 | */ 79 | private void initialize() { 80 | if (mBluetoothLeAdvertiser == null) { 81 | BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 82 | if (bluetoothManager != null) { 83 | BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); 84 | if (bluetoothAdapter != null) { 85 | mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser(); 86 | } 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Starts a delayed Runnable that will cause the BLE Advertising to timeout and stop after a 93 | * set amount of time. 94 | */ 95 | private void setTimeout(){ 96 | mHandler = new Handler(); 97 | timeoutRunnable = new Runnable() { 98 | @Override 99 | public void run() { 100 | stopSelf(); 101 | } 102 | }; 103 | mHandler.postDelayed(timeoutRunnable, TIMEOUT); 104 | } 105 | 106 | /** 107 | * Starts BLE Advertising. 108 | */ 109 | private void startAdvertising() { 110 | 111 | Log.d(MainActivity.TAG, "Service: Starting Advertising"); 112 | 113 | if (mAdvertiseCallback == null) { 114 | AdvertiseSettings settings = buildAdvertiseSettings(); 115 | AdvertiseData data = buildAdvertiseData(); 116 | mAdvertiseCallback = new SampleAdvertiseCallback(); 117 | 118 | if (mBluetoothLeAdvertiser != null) { 119 | mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback); 120 | } 121 | } 122 | } 123 | 124 | 125 | /** 126 | * Stops BLE Advertising. 127 | */ 128 | private void stopAdvertising() { 129 | if (mBluetoothLeAdvertiser != null) { 130 | mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback); 131 | mAdvertiseCallback = null; 132 | } 133 | } 134 | 135 | /** 136 | * Returns an AdvertiseData object which includes the Service UUID and Device Name. 137 | */ 138 | private AdvertiseData buildAdvertiseData() { 139 | 140 | /** 141 | * Note: There is a strict limit of 31 Bytes on packets sent over BLE Advertisements. 142 | * This includes everything put into AdvertiseData including UUIDs, device info, & 143 | * arbitrary service or manufacturer data. 144 | * Attempting to send packets over this limit will result in a failure with error code 145 | * AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE. Catch this error in the 146 | * onStartFailure() method of an AdvertiseCallback implementation. 147 | */ 148 | 149 | AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder(); 150 | //dataBuilder.addServiceUuid(Constants.SERVICE_UUID); 151 | dataBuilder.addServiceUuid(ParcelUuid.fromString(Constants.HEART_RATE_SERVICE_UUID.toString())); 152 | dataBuilder.setIncludeDeviceName(true); 153 | 154 | /* For example - this will cause advertising to fail (exceeds size limit) */ 155 | //String failureData = "asdghkajsghalkxcjhfa;sghtalksjcfhalskfjhasldkjfhdskf"; 156 | //dataBuilder.addServiceData(Constants.SERVICE_UUID, failureData.getBytes()); 157 | 158 | return dataBuilder.build(); 159 | } 160 | 161 | /** 162 | * Returns an AdvertiseSettings object set to use low power (to help preserve battery life) 163 | * and disable the built-in timeout since this code uses its own timeout runnable. 164 | */ 165 | private AdvertiseSettings buildAdvertiseSettings() { 166 | AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder(); 167 | settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER); 168 | settingsBuilder.setTimeout(0); 169 | return settingsBuilder.build(); 170 | } 171 | 172 | /** 173 | * Custom callback after Advertising succeeds or fails to start. Broadcasts the error code 174 | * in an Intent to be picked up by AdvertiserFragment and stops this Service. 175 | */ 176 | private class SampleAdvertiseCallback extends AdvertiseCallback { 177 | 178 | @Override 179 | public void onStartFailure(int errorCode) { 180 | super.onStartFailure(errorCode); 181 | Log.d(MainActivity.TAG, "Advertising failed"); 182 | stopSelf(); 183 | } 184 | 185 | @Override 186 | public void onStartSuccess(AdvertiseSettings settingsInEffect) { 187 | super.onStartSuccess(settingsInEffect); 188 | Log.d(MainActivity.TAG, "Advertising successfully started"); 189 | } 190 | } 191 | 192 | 193 | } 194 | 195 | -------------------------------------------------------------------------------- /app/src/main/java/itan/com/bluetoothle/PeripheralRoleActivity.java: -------------------------------------------------------------------------------- 1 | package itan.com.bluetoothle; 2 | 3 | import android.bluetooth.BluetoothDevice; 4 | import android.bluetooth.BluetoothGatt; 5 | import android.bluetooth.BluetoothGattCharacteristic; 6 | import android.bluetooth.BluetoothGattDescriptor; 7 | import android.bluetooth.BluetoothGattServer; 8 | import android.bluetooth.BluetoothGattServerCallback; 9 | import android.bluetooth.BluetoothGattService; 10 | import android.bluetooth.BluetoothManager; 11 | import android.content.Context; 12 | import android.content.Intent; 13 | import android.os.Bundle; 14 | import android.util.Log; 15 | import android.view.View; 16 | import android.widget.Button; 17 | import android.widget.RadioGroup; 18 | import android.widget.Switch; 19 | 20 | import java.util.Arrays; 21 | import java.util.HashSet; 22 | 23 | import static itan.com.bluetoothle.Constants.BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID; 24 | import static itan.com.bluetoothle.Constants.HEART_RATE_SERVICE_UUID; 25 | import static itan.com.bluetoothle.Constants.SERVER_MSG_FIRST_STATE; 26 | import static itan.com.bluetoothle.Constants.SERVER_MSG_SECOND_STATE; 27 | 28 | 29 | /** 30 | This activity represents the Peripheral/Server role. 31 | Bluetooth communication flow: 32 | 1. advertise [peripheral] 33 | 2. scan [central] 34 | 3. connect [central] 35 | 4. notify [peripheral] 36 | 5. receive [central] 37 | */ 38 | public class PeripheralRoleActivity extends BluetoothActivity implements View.OnClickListener { 39 | 40 | private BluetoothGattService mSampleService; 41 | private BluetoothGattCharacteristic mSampleCharacteristic; 42 | 43 | private BluetoothManager mBluetoothManager; 44 | private BluetoothGattServer mGattServer; 45 | private HashSet mBluetoothDevices; 46 | 47 | private Button mNotifyButton; 48 | private Switch mEnableAdvertisementSwitch; 49 | private RadioGroup mCharacteristicValueSwitch; 50 | 51 | 52 | 53 | @Override 54 | protected void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | 57 | mNotifyButton = (Button) findViewById(R.id.button_notify); 58 | mEnableAdvertisementSwitch = (Switch) findViewById(R.id.advertise_switch); 59 | mCharacteristicValueSwitch = (RadioGroup) findViewById(R.id.color_switch); 60 | 61 | 62 | mNotifyButton.setOnClickListener(this); 63 | mEnableAdvertisementSwitch.setOnClickListener(this); 64 | mCharacteristicValueSwitch.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { 65 | @Override 66 | public void onCheckedChanged(RadioGroup group, int checkedId) { 67 | setCharacteristic(checkedId); 68 | } 69 | }); 70 | 71 | setGattServer(); 72 | setBluetoothService(); 73 | } 74 | 75 | @Override 76 | protected int getLayoutId() { 77 | return R.layout.activity_peripheral_role; 78 | } 79 | 80 | 81 | @Override 82 | public void onClick(View view) { 83 | 84 | switch(view.getId()) { 85 | 86 | case R.id.advertise_switch: 87 | Switch switchToggle = (Switch) view; 88 | if (switchToggle.isChecked()) { 89 | startAdvertising(); 90 | } else { 91 | stopAdvertising(); 92 | } 93 | break; 94 | 95 | 96 | case R.id.button_notify: 97 | notifyCharacteristicChanged(); 98 | break; 99 | 100 | } 101 | } 102 | 103 | 104 | @Override 105 | protected int getTitleString() { 106 | return R.string.peripheral_screen; 107 | } 108 | 109 | 110 | /** 111 | * Starts BLE Advertising by starting {@code PeripheralAdvertiseService}. 112 | */ 113 | private void startAdvertising() { 114 | // TODO bluetooth - maybe bindService? what happens when closing app? 115 | startService(getServiceIntent(this)); 116 | } 117 | 118 | 119 | /** 120 | * Stops BLE Advertising by stopping {@code PeripheralAdvertiseService}. 121 | */ 122 | private void stopAdvertising() { 123 | stopService(getServiceIntent(this)); 124 | mEnableAdvertisementSwitch.setChecked(false); 125 | } 126 | 127 | private void setGattServer() { 128 | 129 | mBluetoothDevices = new HashSet<>(); 130 | mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 131 | 132 | if (mBluetoothManager != null) { 133 | mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback); 134 | } else { 135 | showMsgText(R.string.error_unknown); 136 | } 137 | } 138 | 139 | private void setBluetoothService() { 140 | 141 | // create the Service 142 | mSampleService = new BluetoothGattService(HEART_RATE_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); 143 | 144 | /* 145 | create the Characteristic. 146 | we need to grant to the Client permission to read (for when the user clicks the "Request Characteristic" button). 147 | no need for notify permission as this is an action the Server initiate. 148 | */ 149 | mSampleCharacteristic = new BluetoothGattCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); 150 | setCharacteristic(); // set initial state 151 | 152 | // add the Characteristic to the Service 153 | mSampleService.addCharacteristic(mSampleCharacteristic); 154 | 155 | // add the Service to the Server/Peripheral 156 | if (mGattServer != null) { 157 | mGattServer.addService(mSampleService); 158 | } 159 | } 160 | 161 | 162 | private void setCharacteristic() { 163 | setCharacteristic(R.id.color_option_1); 164 | } 165 | 166 | /* 167 | update the value of Characteristic. 168 | the client will receive the Characteristic value when: 169 | 1. the Client user clicks the "Request Characteristic" button 170 | 2. teh Server user clicks the "Notify Client" button 171 | 172 | value - can be between 0-255 according to: 173 | https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml 174 | */ 175 | private void setCharacteristic(int checkedId) { 176 | /* 177 | done each time the user changes a value of a Characteristic 178 | */ 179 | int value = checkedId == R.id.color_option_1 ? SERVER_MSG_FIRST_STATE : SERVER_MSG_SECOND_STATE; 180 | mSampleCharacteristic.setValue(getValue(value)); 181 | } 182 | 183 | private byte[] getValue(int value) { 184 | return new byte[]{(byte) value}; 185 | } 186 | 187 | /* 188 | send to the client the value of the Characteristic, 189 | as the user requested to notify. 190 | */ 191 | private void notifyCharacteristicChanged() { 192 | /* 193 | done when the user clicks the notify button in the app. 194 | indicate - true for indication (acknowledge) and false for notification (un-acknowledge). 195 | */ 196 | boolean indicate = (mSampleCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE; 197 | 198 | for (BluetoothDevice device : mBluetoothDevices) { 199 | if (mGattServer != null) { 200 | mGattServer.notifyCharacteristicChanged(device, mSampleCharacteristic, indicate); 201 | } 202 | } 203 | } 204 | 205 | /** 206 | * Returns Intent addressed to the {@code PeripheralAdvertiseService} class. 207 | */ 208 | private Intent getServiceIntent(Context context) { 209 | return new Intent(context, PeripheralAdvertiseService.class); 210 | } 211 | 212 | 213 | private final BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() { 214 | 215 | @Override 216 | public void onConnectionStateChange(BluetoothDevice device, final int status, int newState) { 217 | 218 | super.onConnectionStateChange(device, status, newState); 219 | 220 | String msg; 221 | 222 | if (status == BluetoothGatt.GATT_SUCCESS) { 223 | 224 | if (newState == BluetoothGatt.STATE_CONNECTED) { 225 | 226 | mBluetoothDevices.add(device); 227 | 228 | msg = "Connected to device: " + device.getAddress(); 229 | Log.v(MainActivity.TAG, msg); 230 | showMsgText(msg); 231 | 232 | } else if (newState == BluetoothGatt.STATE_DISCONNECTED) { 233 | 234 | mBluetoothDevices.remove(device); 235 | 236 | msg = "Disconnected from device"; 237 | Log.v(MainActivity.TAG, msg); 238 | showMsgText(msg); 239 | } 240 | 241 | } else { 242 | mBluetoothDevices.remove(device); 243 | 244 | msg = getString(R.string.status_error_when_connecting) + ": " + status; 245 | Log.e(MainActivity.TAG, msg); 246 | showMsgText(msg); 247 | 248 | } 249 | } 250 | 251 | 252 | @Override 253 | public void onNotificationSent(BluetoothDevice device, int status) { 254 | super.onNotificationSent(device, status); 255 | Log.v(MainActivity.TAG, "Notification sent. Status: " + status); 256 | } 257 | 258 | 259 | @Override 260 | public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { 261 | 262 | super.onCharacteristicReadRequest(device, requestId, offset, characteristic); 263 | 264 | if (mGattServer == null) { 265 | return; 266 | } 267 | 268 | Log.d(MainActivity.TAG, "Device tried to read characteristic: " + characteristic.getUuid()); 269 | Log.d(MainActivity.TAG, "Value: " + Arrays.toString(characteristic.getValue())); 270 | 271 | mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue()); 272 | } 273 | 274 | 275 | @Override 276 | public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { 277 | 278 | super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); 279 | 280 | Log.v(MainActivity.TAG, "Characteristic Write request: " + Arrays.toString(value)); 281 | 282 | mSampleCharacteristic.setValue(value); 283 | 284 | if (responseNeeded) { 285 | mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value); 286 | } 287 | 288 | } 289 | 290 | @Override 291 | public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) { 292 | 293 | super.onDescriptorReadRequest(device, requestId, offset, descriptor); 294 | 295 | if (mGattServer == null) { 296 | return; 297 | } 298 | 299 | Log.d(MainActivity.TAG, "Device tried to read descriptor: " + descriptor.getUuid()); 300 | Log.d(MainActivity.TAG, "Value: " + Arrays.toString(descriptor.getValue())); 301 | 302 | mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, descriptor.getValue()); 303 | } 304 | 305 | @Override 306 | public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, 307 | BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, 308 | int offset, 309 | byte[] value) { 310 | 311 | super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); 312 | 313 | Log.v(MainActivity.TAG, "Descriptor Write Request " + descriptor.getUuid() + " " + Arrays.toString(value)); 314 | 315 | // int status = BluetoothGatt.GATT_SUCCESS; 316 | // if (descriptor.getUuid() == CLIENT_CHARACTERISTIC_CONFIGURATION_UUID) { 317 | // BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic(); 318 | // boolean supportsNotifications = (characteristic.getProperties() & 319 | // BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0; 320 | // boolean supportsIndications = (characteristic.getProperties() & 321 | // BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0; 322 | // 323 | // if (!(supportsNotifications || supportsIndications)) { 324 | // status = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED; 325 | // } else if (value.length != 2) { 326 | // status = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH; 327 | // } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) { 328 | // status = BluetoothGatt.GATT_SUCCESS; 329 | // mCurrentServiceFragment.notificationsDisabled(characteristic); 330 | // descriptor.setValue(value); 331 | // } else if (supportsNotifications && 332 | // Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) { 333 | // status = BluetoothGatt.GATT_SUCCESS; 334 | // mCurrentServiceFragment.notificationsEnabled(characteristic, false /* indicate */); 335 | // descriptor.setValue(value); 336 | // } else if (supportsIndications && 337 | // Arrays.equals(value, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)) { 338 | // status = BluetoothGatt.GATT_SUCCESS; 339 | // mCurrentServiceFragment.notificationsEnabled(characteristic, true /* indicate */); 340 | // descriptor.setValue(value); 341 | // } else { 342 | // status = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED; 343 | // } 344 | // } else { 345 | // status = BluetoothGatt.GATT_SUCCESS; 346 | // descriptor.setValue(value); 347 | // } 348 | // if (responseNeeded) { 349 | // mGattServer.sendResponse(device, requestId, status, 350 | // /* No need to respond with offset */ 0, 351 | // /* No need to respond with a value */ null); 352 | // } 353 | 354 | } 355 | }; 356 | 357 | 358 | } 359 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itanbp/android-ble-peripheral-central/7be1191deaaec28f6301a4e573a5cf06b7f98426/app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itanbp/android-ble-peripheral-central/7be1191deaaec28f6301a4e573a5cf06b7f98426/app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itanbp/android-ble-peripheral-central/7be1191deaaec28f6301a4e573a5cf06b7f98426/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itanbp/android-ble-peripheral-central/7be1191deaaec28f6301a4e573a5cf06b7f98426/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itanbp/android-ble-peripheral-central/7be1191deaaec28f6301a4e573a5cf06b7f98426/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/color_option_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/color_option_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_central_role.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 45 | 46 | 47 |