diff --git a/app/build.gradle b/app/build.gradle index 8ca843c..3fcee85 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,21 @@ apply plugin: 'com.android.application' +apply plugin: 'me.tatarka.retrolambda' android { - compileSdkVersion 18 - buildToolsVersion "21.1.2" + compileSdkVersion 24 + buildToolsVersion "24.0.0" defaultConfig { applicationId "com.dataart.btle_android" minSdkVersion 18 - targetSdkVersion 21 + targetSdkVersion 24 versionCode 2 versionName "2.0" + + multiDexEnabled true + } + dexOptions { + javaMaxHeapSize "4g" } buildTypes { release { @@ -18,15 +24,23 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } + useLibrary 'org.apache.http.legacy' } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:support-v4:21.0.3' - compile 'com.google.code.gson:gson:2.3' + compile 'com.android.support:appcompat-v7:24.0.0' + compile 'com.android.support:design:24.0.0' + compile 'com.google.code.gson:gson:2.4' compile 'com.jakewharton.timber:timber:2.7.1' + provided "org.projectlombok:lombok:1.12.6" compile 'commons-codec:commons-codec:1.5' + compile 'com.google.android.gms:play-services:9.0.2' + compile 'io.reactivex:rxandroid:1.1.0' + compile 'com.google.guava:guava:19.0' + + compile 'com.android.support:multidex:1.0.1' } diff --git a/app/src/androidTest/java/com/dataart/btle_android/ApplicationTest.java b/app/src/androidTest/java/com/dataart/btle_android/ApplicationTest.java deleted file mode 100644 index e4aae39..0000000 --- a/app/src/androidTest/java/com/dataart/btle_android/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dataart.btle_android; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9eab165..8d87451 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,12 +12,15 @@ + + + + android:theme="@style/AppTheme"> diff --git a/app/src/main/java/com/dataart/btle_android/BTLEApplication.java b/app/src/main/java/com/dataart/btle_android/BTLEApplication.java index 0de2d65..6894f18 100644 --- a/app/src/main/java/com/dataart/btle_android/BTLEApplication.java +++ b/app/src/main/java/com/dataart/btle_android/BTLEApplication.java @@ -1,6 +1,6 @@ package com.dataart.btle_android; -import android.app.Application; +import android.support.multidex.MultiDexApplication; import com.dataart.btle_android.devicehive.BTLEDeviceHive; @@ -8,12 +8,16 @@ * Created by alrybakov */ -public class BTLEApplication extends Application { +public class BTLEApplication extends MultiDexApplication { private static BTLEApplication application; private BTLEDeviceHive device; + public static BTLEApplication getApplication() { + return application; + } + @Override public void onCreate() { super.onCreate(); @@ -22,10 +26,6 @@ public void onCreate() { device = BTLEDeviceHive.newInstance(getApplicationContext()); } - public static BTLEApplication getApplication() { - return application; - } - public BTLEDeviceHive getDevice() { return device; } diff --git a/app/src/main/java/com/dataart/btle_android/MainActivity.java b/app/src/main/java/com/dataart/btle_android/MainActivity.java index 03844a9..88e9d15 100644 --- a/app/src/main/java/com/dataart/btle_android/MainActivity.java +++ b/app/src/main/java/com/dataart/btle_android/MainActivity.java @@ -1,72 +1,127 @@ package com.dataart.btle_android; -import android.app.Activity; import android.app.ActivityManager; +import android.app.AlertDialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; -import android.util.Log; -import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; -import android.widget.Toast; import com.dataart.android.devicehive.Notification; import com.dataart.btle_android.btle_gateway.BluetoothLeService; import com.dataart.btle_android.devicehive.BTLEDeviceHive; import com.dataart.btle_android.devicehive.BTLEDevicePreferences; +import com.dataart.btle_android.helpers.BleHelpersFactory; +import com.dataart.btle_android.helpers.ble.base.BleInitializer; import timber.log.Timber; -public class MainActivity extends Activity implements BTLEDeviceHive.NotificationListener { +public class MainActivity extends AppCompatActivity implements BTLEDeviceHive.NotificationListener { - private static final String TAG = MainActivity.class.getName(); - - private static final int REQUEST_ENABLE_BT = 1; + private BleInitializer bleInitializer; private BluetoothManager mBluetoothManager; - private BluetoothAdapter mBluetoothAdapter; - private EditText serverUrlEditText; private EditText gatewayIdEditText; private EditText accessKeyEditText; private TextView hintText; private Button serviceButton; private Button restartServiceButton; - private BTLEDevicePreferences prefs; - private boolean isServiceStarted; + private final View.OnClickListener restartClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + saveValues(); + BluetoothLeService.stop(MainActivity.this); + BluetoothLeService.start(MainActivity.this); + restartServiceButton.setVisibility(View.GONE); + serviceButton.setVisibility(View.VISIBLE); + onServiceRunning(); + hintText.setVisibility(View.GONE); + } + }; + private final TextView.OnEditorActionListener changeListener = (textView, actionId, keyEvent) -> { + if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_NEXT) { + onDataChanged(); + } + return false; + }; + private final TextWatcher changeWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void afterTextChanged(Editable editable) { + onDataChanged(); + } + }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); + Toolbar myToolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(myToolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setTitle(R.string.app_name); + } + Timber.plant(new Timber.DebugTree()); - if (getActionBar() != null) { - getActionBar().setTitle(R.string.app_name); +// Warn if developer tries to lower SDK version + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + alertSdkVersionMismatch(() -> { + finish(); + System.exit(0); + }); + + return; } +// BleInitializer will start service on initialization success + bleInitializer = BleHelpersFactory.getInitializer(this, bluetoothAdapter -> startService()); + + init(); + } + + private void fatalDialog(int message) { + new AlertDialog.Builder(this) + .setTitle(R.string.unsupported) + .setMessage(message) + .setPositiveButton(android.R.string.ok, (dialog, which) -> finish()) + .create().show(); + } + + private void init() { + if (!isBluetoothLeSupported()) { - Toast.makeText(this, R.string.error_message_btle_not_supported, Toast.LENGTH_SHORT).show(); - finish(); + fatalDialog(R.string.error_message_btle_not_supported); + return; } if (!isBluetoothSupported()) { - Toast.makeText(this, R.string.error_message_bt_not_supported, Toast.LENGTH_SHORT).show(); - finish(); + fatalDialog(R.string.error_message_bt_not_supported); + return; } prefs = new BTLEDevicePreferences(); @@ -79,9 +134,15 @@ protected void onCreate(Bundle savedInstanceState) { resetValues(); serviceButton = (Button) findViewById(R.id.service_button); - serviceButton.setOnClickListener(serviceClickListener); + //noinspection ConstantConditions + serviceButton.setOnClickListener(v -> { + if (validateValues()) { + bleInitializer.start(); + } + }); restartServiceButton = (Button) findViewById(R.id.save_button); + //noinspection ConstantConditions restartServiceButton.setOnClickListener(restartClickListener); serverUrlEditText.setOnEditorActionListener(changeListener); @@ -96,76 +157,44 @@ protected void onCreate(Bundle savedInstanceState) { if (isLeServiceRunning()) { onServiceRunning(); } - - registerReceiver(mReceiver, new IntentFilter(BluetoothLeService.ACTION_BT_PERMISSION_REQUEST)); } - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (BluetoothLeService.ACTION_BT_PERMISSION_REQUEST.equals(action)) { - requestEnableBluetooth(); - } - } - }; - private boolean isBluetoothLeSupported() { return getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); } - public boolean isBluetoothSupported() { + private boolean isBluetoothSupported() { if (mBluetoothManager == null) { mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (mBluetoothManager == null) { - Log.e(TAG, "Unable to initialize BluetoothManager"); + Timber.e(getString(R.string.bt_unable_init)); return false; } } - mBluetoothAdapter = mBluetoothManager.getAdapter(); + BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { - Log.e(TAG, "Unable to obtain a BluetoothAdapter"); + Timber.e(getString(R.string.bt_unable_get_btm)); return false; } return true; } - private void requestEnableBluetooth() { - final Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); - } - @Override protected void onResume() { super.onResume(); - - // Ensures Bluetooth is enabled on the device. If Bluetooth is not currently enabled, - // fire an intent to display a dialog asking the user to grant permission to enable it. - if (!mBluetoothAdapter.isEnabled()) { - requestEnableBluetooth(); - } + bleInitializer.onResume(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - // User chose not to enable Bluetooth. - if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) { - if (isServiceStarted) { - BluetoothLeService.stop(this); - } - finish(); - return; - } + bleInitializer.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data); } @Override - protected void onDestroy() { - if (mReceiver != null) { - unregisterReceiver(mReceiver); - } - super.onDestroy(); + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + bleInitializer.onRequestPermissionsResult(requestCode, permissions, grantResults); + super.onRequestPermissionsResult(requestCode, permissions, grantResults); } private boolean isLeServiceRunning() { @@ -178,20 +207,16 @@ private boolean isLeServiceRunning() { return false; } - private final View.OnClickListener serviceClickListener = new View.OnClickListener() { - - @Override - public void onClick(View v) { - if (!isServiceStarted) { - saveValues(); - onServiceRunning(); - BluetoothLeService.start(MainActivity.this); - } else { - onServiceStopped(); - BluetoothLeService.stop(MainActivity.this); - } + private void startService() { + if (!isServiceStarted) { + saveValues(); + onServiceRunning(); + BluetoothLeService.start(MainActivity.this); + } else { + onServiceStopped(); + BluetoothLeService.stop(MainActivity.this); } - }; + } private void onServiceRunning() { isServiceStarted = true; @@ -203,19 +228,6 @@ private void onServiceStopped() { serviceButton.setText(R.string.button_start); } - private final View.OnClickListener restartClickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - saveValues(); - BluetoothLeService.stop(MainActivity.this); - BluetoothLeService.start(MainActivity.this); - restartServiceButton.setVisibility(View.GONE); - serviceButton.setVisibility(View.VISIBLE); - onServiceRunning(); - hintText.setVisibility(View.GONE); - } - }; - private boolean isRestartRequired() { final String newUrl = serverUrlEditText.getText().toString(); final String newGatewayId = gatewayIdEditText.getText().toString(); @@ -238,54 +250,67 @@ private void onDataChanged() { } private void resetValues() { - serverUrlEditText.setText(prefs.getServerUrl()); - gatewayIdEditText.setText(TextUtils.isEmpty(prefs.getGatewayId()) ? - getString(R.string.default_gateway_id) : prefs.getGatewayId()); - accessKeyEditText.setText(TextUtils.isEmpty(prefs.getAccessKey()) ? - "" : prefs.getAccessKey()); + String serverUrl = prefs.getServerUrl(); + serverUrlEditText.setText( + TextUtils.isEmpty(serverUrl) + ? getString(R.string.default_server_url) + : serverUrl + ); + + String gatewayId = prefs.getGatewayId(); + gatewayIdEditText.setText( + TextUtils.isEmpty(gatewayId) + ? getString(R.string.default_gateway_id) + : gatewayId + ); + + String accessKey = prefs.getAccessKey(); + accessKeyEditText.setText( + TextUtils.isEmpty(accessKey) + ? "" + : accessKey + ); } - private void saveValues() { + private void resetErrors() { + serverUrlEditText.setError(null); + gatewayIdEditText.setError(null); + accessKeyEditText.setError(null); + } + + private boolean validateValues() { + resetErrors(); + final String serverUrl = serverUrlEditText.getText().toString(); final String gatewayId = gatewayIdEditText.getText().toString(); final String accessKey = accessKeyEditText.getText().toString(); + if (TextUtils.isEmpty(serverUrl)) { serverUrlEditText.setError(getString(R.string.error_message_empty_server_url)); + serverUrlEditText.requestFocus(); } else if (TextUtils.isEmpty(gatewayId)) { gatewayIdEditText.setError(getString(R.string.error_message_empty_gateway_id)); + gatewayIdEditText.requestFocus(); } else if (TextUtils.isEmpty(accessKey)) { accessKeyEditText.setError(getString(R.string.error_message_empty_accesskey)); + accessKeyEditText.requestFocus(); } else { - prefs.setAccessKeySync(accessKey); - prefs.setServerUrlSync(serverUrl); - prefs.setGatewayIdSync(gatewayId); + return true; } - } - private final TextView.OnEditorActionListener changeListener = new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { - if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_NEXT) { - onDataChanged(); - } - return false; - } - }; + return false; + } - private final TextWatcher changeWatcher = new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } + private void saveValues() { + final String serverUrl = serverUrlEditText.getText().toString(); + final String gatewayId = gatewayIdEditText.getText().toString(); + final String accessKey = accessKeyEditText.getText().toString(); - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } + prefs.setAccessKeySync(accessKey); + prefs.setServerUrlSync(serverUrl); + prefs.setGatewayIdSync(gatewayId); - @Override - public void afterTextChanged(Editable editable) { - onDataChanged(); - } - }; + } @Override public void onDeviceSentNotification(Notification notification) { @@ -297,4 +322,24 @@ public void onDeviceFailedToSendNotification(Notification notification) { } + @Override + protected void onStart() { + super.onStart(); + bleInitializer.onStart(); + } + + @Override + protected void onStop() { + bleInitializer.onStop(); + super.onStop(); + } + + private void alertSdkVersionMismatch(final Runnable runnable) { + new AlertDialog.Builder(this) + .setTitle(R.string.sdk_version_warning_title) + .setMessage(R.string.sdk_version_warning) + .setPositiveButton(android.R.string.ok, (dialog, which) -> runnable.run()) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } } diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/BTLEGateway.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/BTLEGateway.java index dd44b9b..48286f2 100644 --- a/app/src/main/java/com/dataart/btle_android/btle_gateway/BTLEGateway.java +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/BTLEGateway.java @@ -8,11 +8,12 @@ import com.dataart.android.devicehive.Notification; import com.dataart.android.devicehive.device.CommandResult; import com.dataart.android.devicehive.device.future.CmdResFuture; -import com.dataart.btle_android.R; import com.dataart.android.devicehive.device.future.SimpleCallableFuture; -import com.dataart.btle_android.btle_gateway.gatt.callbacks.CmdResult; -import com.dataart.btle_android.btle_gateway.gatt.callbacks.InteractiveGattCallback; +import com.dataart.btle_android.R; +import com.dataart.btle_android.btle_gateway.gateway_helpers.ValidationHelper; +import com.dataart.btle_android.btle_gateway.gatt_callbacks.CmdResult; import com.dataart.btle_android.devicehive.BTLEDeviceHive; +import com.google.common.base.Optional; import com.google.gson.Gson; import java.util.ArrayList; @@ -30,6 +31,8 @@ public BTLEGateway(BluetoothServer bluetoothServer) { } public SimpleCallableFuture doCommand(final Context context, final BTLEDeviceHive dh, final Command command) { + ValidationHelper validationHelper = new ValidationHelper(context); + try { Timber.d("doCommand"); final String name = command.getCommand(); @@ -42,6 +45,8 @@ public SimpleCallableFuture doCommand(final Context context, fina final String serviceUUID = (params != null) ? (String) params.get("serviceUUID") : null; final String characteristicUUID = (params != null) ? (String) params.get("characteristicUUID") : null; + Optional validationError = Optional.absent(); + Timber.d("switch"); switch (leCommand) { case SCAN_START: @@ -55,31 +60,48 @@ public SimpleCallableFuture doCommand(final Context context, fina return scanAndReturnResults(dh); case GATT_CONNECT: - Timber.d("connecting to " + address); - return bluetoothServerGateway.gattConnect(address, new InteractiveGattCallback.DisconnectListener() { + validationError = validationHelper.validateAddress(leCommand.getCommand(), address); + if (validationError.isPresent()) { + return validationError.get(); + } - @Override - public void onDisconnect() { - final String json = new Gson().toJson(String.format(context.getString(R.string.is_disconnected), address)); - sendNotification(dh, leCommand, json); - } + Timber.d("Connecting to " + address); + return bluetoothServerGateway.gattConnect(address, () -> { + final String json = new Gson().toJson(String.format(context.getString(R.string.is_disconnected), address)); + sendNotification(dh, leCommand, json); }); case GATT_DISCONNECT: - Timber.d("disconnecting from" + address); + validationError = validationHelper.validateAddress(leCommand.getCommand(), address); + if (validationError.isPresent()) { + return validationError.get(); + } + + Timber.d("Disconnecting from" + address); return bluetoothServerGateway.gattDisconnect(address); case GATT_PRIMARY: + validationError = validationHelper.validateAddress(leCommand.getCommand(), address); + if (validationError.isPresent()) { + return validationError.get(); + } + return gattPrimary(address, dh, leCommand); case GATT_CHARACTERISTICS: + validationError = validationHelper.validateCharacteristics(leCommand.getCommand(), address, serviceUUID); + if (validationError.isPresent()) { + return validationError.get(); + } + return gattCharacteristics(address, dh, leCommand); case GATT_READ: { - CmdResFuture future = validateArgs(address, serviceUUID, characteristicUUID); - if (future != null) { - return future; + validationError = validationHelper.validateRead(leCommand.getCommand(), address, serviceUUID, characteristicUUID); + if (validationError.isPresent()) { + return validationError.get(); } + return bluetoothServerGateway.gattRead(address, serviceUUID, characteristicUUID, new GattCharacteristicCallBack() { @Override public void onRead(byte[] value) { @@ -93,9 +115,10 @@ public void onRead(byte[] value) { case GATT_WRITE: { final String sValue = (String) (params != null ? params.get("value") : null); - CmdResFuture future = validateArgs(address, serviceUUID, characteristicUUID, sValue); - if (future != null) { - return future; + + validationError = validationHelper.validateWrite(leCommand.getCommand(), address, serviceUUID, characteristicUUID, sValue); + if (validationError.isPresent()) { + return validationError.get(); } final byte[] value = Utils.parseHexBinary(sValue); @@ -109,6 +132,11 @@ public void onWrite(int state) { }); } case GATT_NOTIFICATION: + validationError = validationHelper.validateNotifications(leCommand.getCommand(), address, serviceUUID); + if (validationError.isPresent()) { + return validationError.get(); + } + return bluetoothServerGateway.gattNotifications(context, address, serviceUUID, characteristicUUID, true, new GattCharacteristicCallBack() { @Override public void onRead(byte[] value) { @@ -119,6 +147,11 @@ public void onRead(byte[] value) { }); case GATT_NOTIFICATION_STOP: + validationError = validationHelper.validateNotifications(leCommand.getCommand(), address, serviceUUID); + if (validationError.isPresent()) { + return validationError.get(); + } + return bluetoothServerGateway.gattNotifications(context, address, serviceUUID, characteristicUUID, false, new GattCharacteristicCallBack() { @Override @@ -134,7 +167,7 @@ public void onRead(byte[] value) { } } catch (Exception e) { Timber.e("error:"+e.toString()); -// Log.e("TAG", "Error during handling" + e.toString()); + final Notification notification = new Notification("Error", e.toString()); dh.sendNotification(notification); return new SimpleCallableFuture<>(CmdResult.failWithStatus("Error: \"" + e.toString() + "\"")); @@ -144,41 +177,12 @@ public void onRead(byte[] value) { return new SimpleCallableFuture<>(CmdResult.success()); } - private CmdResFuture validateArgs(String address, String serviceUUID, String characteristicUUID, String value) { - CmdResFuture future=validateArgs(address, serviceUUID, characteristicUUID); - if(future!=null){ - return future; - } - if(value==null){ - return new CmdResFuture(CmdResult.failWithStatus(R.string.fail_val)); - } - return null; - } - - private CmdResFuture validateArgs(String address, String serviceUUID, String characteristicUUID){ - if(address==null){ - return new CmdResFuture(CmdResult.failWithStatus(R.string.fail_address)); - } - if(serviceUUID==null){ - return new CmdResFuture(CmdResult.failWithStatus(R.string.fail_service)); - } - if(characteristicUUID==null){ - return new CmdResFuture(CmdResult.failWithStatus(R.string.fail_characteristic)); - } - return null; - } - private SimpleCallableFuture scanAndReturnResults(final BTLEDeviceHive dh) { final SimpleCallableFuture future = new SimpleCallableFuture<>(); bluetoothServerGateway.scanStart(); Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { - sendStopResult(dh, future); - } - }, BluetoothServer.COMMAND_SCAN_DELAY); + handler.postDelayed(() -> sendStopResult(dh, future), BluetoothServer.COMMAND_SCAN_DELAY); return future; } @@ -208,7 +212,7 @@ private void sendStopResult(BTLEDeviceHive dh, SimpleCallableFuture gattPrimary(String address, final BTLEDeviceHive dh, final LeCommand leCommand) { + private SimpleCallableFuture gattPrimary(String address, @SuppressWarnings("UnusedParameters") final BTLEDeviceHive dh, @SuppressWarnings("UnusedParameters") final LeCommand leCommand) { final CmdResFuture future = new CmdResFuture(); bluetoothServerGateway.gattPrimary(address, new GattCharacteristicCallBack() { @Override @@ -219,7 +223,7 @@ public void onServices(List uuidList) { return future; } - private SimpleCallableFuture gattCharacteristics(String address, final BTLEDeviceHive dh, final LeCommand leCommand) { + private SimpleCallableFuture gattCharacteristics(String address, @SuppressWarnings("UnusedParameters") final BTLEDeviceHive dh, @SuppressWarnings("UnusedParameters") final LeCommand leCommand) { final CmdResFuture future = new CmdResFuture(); bluetoothServerGateway.gattCharacteristics(address, new GattCharacteristicCallBack() { @Override diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/BTLEScannerOld.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/BTLEScannerOld.java new file mode 100644 index 0000000..756383a --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/BTLEScannerOld.java @@ -0,0 +1,84 @@ +package com.dataart.btle_android.btle_gateway; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanResult; + +import java.util.List; + +import lombok.Data; + +import static android.os.Build.VERSION; +import static android.os.Build.VERSION_CODES; + +/** + * Created by Constantine Mars on 6/3/16. + * Abstracted BTLE scanning manager that encapsulates both approaches for android before L and starting with L + */ + +@Data +public class BTLEScannerOld { + + + private final BluetoothAdapter bluetoothAdapter; + private final BluetoothAdapter.LeScanCallback leScanCallback; + private final android.bluetooth.le.ScanCallback scanCallback; + private final BluetoothLeScanner leScanner; + + public BTLEScannerOld(BluetoothAdapter bluetoothAdapter, Callback callback) { + this.bluetoothAdapter = bluetoothAdapter; + this.leScanCallback = callback::onScan; + + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + this.leScanner = bluetoothAdapter.getBluetoothLeScanner(); + + this.scanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + callback.onScan(result.getDevice(), result.getRssi(), result.getScanRecord().getBytes()); + } + + super.onScanResult(callbackType, result); + } + }; + } else { + this.leScanner = null; + this.scanCallback = null; + } + } + + public void startScan() { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + leScanner.startScan(new android.bluetooth.le.ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + + super.onScanResult(callbackType, result); + } + + @Override + public void onBatchScanResults(List results) { + super.onBatchScanResults(results); + } + }); + } else { + bluetoothAdapter.startLeScan(leScanCallback); + } + } + + public void stopScan() { + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + leScanner.stopScan(this.scanCallback); + } else { + bluetoothAdapter.stopLeScan(leScanCallback); + } + } + + interface Callback { + void onScan(BluetoothDevice device, int rssi, byte[] scanRecord); + } + +} diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/BluetoothServer.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/BluetoothServer.java index 0a00971..48e552c 100644 --- a/app/src/main/java/com/dataart/btle_android/btle_gateway/BluetoothServer.java +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/BluetoothServer.java @@ -18,9 +18,11 @@ import com.dataart.android.devicehive.device.future.SimpleCallableFuture; import com.dataart.btle_android.BTLEApplication; import com.dataart.btle_android.R; -import com.dataart.btle_android.btle_gateway.gatt.callbacks.CmdResult; -import com.dataart.btle_android.btle_gateway.gatt.callbacks.DeviceConnection; -import com.dataart.btle_android.btle_gateway.gatt.callbacks.InteractiveGattCallback; +import com.dataart.btle_android.btle_gateway.gatt_callbacks.CmdResult; +import com.dataart.btle_android.btle_gateway.gatt_callbacks.DeviceConnection; +import com.dataart.btle_android.btle_gateway.gatt_callbacks.InteractiveGattCallback; +import com.dataart.btle_android.helpers.BleHelpersFactory; +import com.dataart.btle_android.helpers.ble.base.BleScanner; import org.apache.commons.codec.binary.Hex; @@ -48,19 +50,15 @@ public class BluetoothServer extends BluetoothGattCallback { private ArrayList deviceList = new ArrayList<>(); private DiscoveredDeviceListener discoveredDeviceListener; - private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() { - @Override - public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { - onDeviceFound(device, rssi, scanRecord); - } - }; + private BleScanner scanner; + private BleScanner.ScanCallback scanCallback = this::onDeviceFound; public BluetoothServer(Context context) { this.context = context; } - private BluetoothAdapter bluetoothAdapter() { + private BluetoothAdapter getBluetoothAdapter() { if (bluetoothAdapter == null) { BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter(); @@ -104,22 +102,35 @@ private LeScanResult onDeviceFound(BluetoothDevice device, int rssi, byte[] scan } public void scanStart() { - Timber.d("BLE scan started..."); - bluetoothAdapter().startLeScan(leScanCallback); + Timber.d("BLE startScan started..."); + + if (scanner == null) { + scanner = getScanner(); + } + + scanner.startScan(); + final Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { -// "Never scan on a loop, and set a time limit on your scan. " - https://developer.android.com/guide/topics/connectivity/bluetooth-le.html#find - bluetoothAdapter().stopLeScan(leScanCallback); - Timber.d("BLE scan stopped on timeout " + BluetoothServer.COMMAND_SCAN_DELAY / 1000 + " sec"); - } + handler.postDelayed(() -> { + +// "Never startScan on a loop, and set a time limit on your startScan. " - https://developer.android.com/guide/topics/connectivity/bluetooth-le.html#find + scanner.stopScan(); + Timber.d("BLE startScan stopped on timeout " + BluetoothServer.COMMAND_SCAN_DELAY / 1000 + " sec"); + }, BluetoothServer.COMMAND_SCAN_DELAY); } + private BleScanner getScanner() { + if (scanner == null) { + scanner = BleHelpersFactory.getScanner(scanCallback, getBluetoothAdapter()); + } + + return scanner; + } + public void scanStop() { Log.d(TAG, "Stop BLE Scan"); - bluetoothAdapter().stopLeScan(leScanCallback); + scanner.stopScan(); } protected void addDevice(final LeScanResult device) { @@ -195,18 +206,15 @@ public void fail(String message) { @Override public void call(BluetoothDevice device) { Timber.d("device found. connecting"); - connectAndSave(address, device, new InteractiveGattCallback.OnConnectedListener() { - @Override - public void call() { - Timber.d("calling operation on successfull connection"); - applyForConnection(address, operation); - } + connectAndSave(address, device, () -> { + Timber.d("calling operation on successfull connection"); + applyForConnection(address, operation); }); } @Override public void fail(String message) { - Timber.d("device " + address + " not found - try to scan once more"); + Timber.d("device " + address + " not found - try to startScan once more"); operation.fail(message); } }); @@ -447,28 +455,26 @@ public interface DiscoveredDeviceListener { private class ScanCallbacks { private String address; private ConnectionOperation operation; - private BluetoothAdapter.LeScanCallback localCallback; + private BleScanner.ScanCallback localCallback; + private BleScanner scanner; public ScanCallbacks(final String address, final ConnectionOperation operation) { this.address = address; this.operation = operation; - this.localCallback = new BluetoothAdapter.LeScanCallback() { + + this.localCallback = new BleScanner.ScanCallback() { private boolean found = false; @Override - public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) { - - if (!found && bluetoothDevice.getAddress().equals(address)) { + public void onDeviceFound(BluetoothDevice device, int rssi, byte[] scanRecord) { + if (!found && device.getAddress().equals(address)) { found = true; stop(); - onDeviceFound(bluetoothDevice, i, bytes); + onDeviceFound(device, rssi, scanRecord); - connectAndSave(address, bluetoothDevice, new InteractiveGattCallback.OnConnectedListener() { - @Override - public void call() { - Timber.d("on connected - calling operation"); - applyForConnection(address, operation); - } + connectAndSave(address, device, () -> { + Timber.d("on connected - calling operation"); + applyForConnection(address, operation); }); } } @@ -477,25 +483,23 @@ public void call() { public void start() { Timber.d("no device or connection - scanning for device"); -// scan for device, add it to discovered devices, connect and call operation +// startScan for device, add it to discovered devices, connect and call operation - bluetoothAdapter().startLeScan(localCallback); + scanner = BleHelpersFactory.getScanner(localCallback, getBluetoothAdapter()); + scanner.startScan(); final Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { -// "Never scan on a loop, and set a time limit on your scan. " - https://developer.android.com/guide/topics/connectivity/bluetooth-le.html#find - Timber.d("on timeout"); - stop(); - operation.fail(BTLEApplication.getApplication().getString(R.string.status_notfound_timeout)); - } + handler.postDelayed(() -> { +// "Never startScan on a loop, and set a time limit on your startScan. " - https://developer.android.com/guide/topics/connectivity/bluetooth-le.html#find + Timber.d("on timeout"); + stop(); + operation.fail(BTLEApplication.getApplication().getString(R.string.status_notfound_timeout)); }, BluetoothServer.COMMAND_SCAN_DELAY); } public void stop() { Timber.d("stop"); - bluetoothAdapter().stopLeScan(localCallback); + scanner.stopScan(); } } diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/build.gradle b/app/src/main/java/com/dataart/btle_android/btle_gateway/build.gradle deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/gateway_helpers/ValidationHelper.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/gateway_helpers/ValidationHelper.java new file mode 100644 index 0000000..e2815d3 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/gateway_helpers/ValidationHelper.java @@ -0,0 +1,74 @@ +package com.dataart.btle_android.btle_gateway.gateway_helpers; + +import android.content.Context; + +import com.dataart.android.devicehive.device.future.CmdResFuture; +import com.dataart.btle_android.R; +import com.dataart.btle_android.btle_gateway.gatt_callbacks.CmdResult; +import com.google.common.base.Optional; + +import lombok.RequiredArgsConstructor; + +/** + * Created by Constantine Mars on 6/14/16. + *

+ * Validation utils + */ + +@RequiredArgsConstructor +public class ValidationHelper { + private static final String VALUE_REGEX = "([a-fA-F0-9]{2}){1,}"; + private static final String ADDRESS_REGEX = "(([a-fA-F0-9]{2}:){5})([a-fA-F0-9]{2})"; + private static final String SERVICE_CHARACTERISTIC_UUID_REGEX = "([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})|([a-fA-F0-9]{4})"; + private final Context context; + + public Optional validateAddress(final String command, final String address) { + return validate(command, R.string.cmd_requires_address, address, ADDRESS_REGEX); + } + + public Optional validateServiceUUID(final String command, final String serviceUUID) { + return validate(command, R.string.cmd_requires_service_uuid, serviceUUID, SERVICE_CHARACTERISTIC_UUID_REGEX); + } + + public Optional validateCharacteristicUUID(final String command, final String serviceUUID) { + return validate(command, R.string.cmd_requires_characteristic_uuid, serviceUUID, SERVICE_CHARACTERISTIC_UUID_REGEX); + } + + public Optional validateValue(final String command, final String value) { + return validate(command, R.string.cmd_requires_value, value, VALUE_REGEX); + } + + public Optional validate(final String command, int messageResId, final String value, final String regex) { + if (value == null || !value.matches(regex)) { + return Optional.of(new CmdResFuture( + CmdResult.failWithStatus(context.getString(messageResId, command)) + )); + } + + return Optional.absent(); + } + + public Optional validateCharacteristics(final String command, final String address, final String serviceUUID) { + return validateAddress(command, address) + .or(validateServiceUUID(command, serviceUUID)); + } + + public Optional validateNotifications(final String command, final String address, final String serviceUUID) { + return validateAddress(command, address) + .or(validateServiceUUID(command, serviceUUID)); + } + + public Optional validateRead(final String command, final String address, final String serviceUUID, final String characteristicUUID) { + return validateAddress(command, address) + .or(validateServiceUUID(command, serviceUUID)) + .or(validateCharacteristicUUID(command, characteristicUUID)); + } + + + public Optional validateWrite(String command, String address, String serviceUUID, String characteristicUUID, String value) { + return validateAddress(command, address) + .or(validateServiceUUID(command, serviceUUID)) + .or(validateCharacteristicUUID(command, characteristicUUID)) + .or(validateValue(command, value)); + } +} diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/CmdResult.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/CmdResult.java similarity index 97% rename from app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/CmdResult.java rename to app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/CmdResult.java index d3ca8d5..4b545d2 100644 --- a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/CmdResult.java +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/CmdResult.java @@ -1,4 +1,4 @@ -package com.dataart.btle_android.btle_gateway.gatt.callbacks; +package com.dataart.btle_android.btle_gateway.gatt_callbacks; import android.content.Context; @@ -16,6 +16,7 @@ public class CmdResult { protected String serviceUUID; protected String characteristicUUID; protected String device; + protected Context context; public CmdResult(String serviceUUID, String characteristicUUID, String device, Context context) { this.serviceUUID = serviceUUID; @@ -24,12 +25,6 @@ public CmdResult(String serviceUUID, String characteristicUUID, String device, C this.context = context; } - protected Context context; - - private String jsonFullStatus(int statusStringId) { - return jsonFullStatus(context.getString(statusStringId)); - } - private static String jsonStatusOk() { return new Gson().toJson(StatusJson.Status.statusOk()); } @@ -50,6 +45,30 @@ private static String jsonStatus(String status){ return new Gson().toJson(new StatusJson.Status(status)); } + public static CommandResult success() { + return new CommandResult(CommandResult.STATUS_COMLETED, jsonStatusOk()); + } + + public static CommandResult successWithObject(Object object) { + return new CommandResult(CommandResult.STATUS_COMLETED, jsonStatusWithObject(object)); + } + + public static CommandResult failWithStatus(String status) { + return new CommandResult(CommandResult.STATUS_FAILED, jsonStatus(status)); + } + + public static CommandResult failWithStatus(int strResId) { + return new CommandResult(CommandResult.STATUS_FAILED, jsonStatus(strResId)); + } + + public static CommandResult failTimeoutReached() { + return new CommandResult(CommandResult.STATUS_FAILED, jsonStatusTimeoutReached()); + } + + private String jsonFullStatus(int statusStringId) { + return jsonFullStatus(context.getString(statusStringId)); + } + private String jsonFullStatus(String status) { return new Gson().toJson(new StatusJson.FullStatus( status, @@ -73,30 +92,10 @@ private String jsonFullStatusWithVal(int statusResId, String val) { return jsonFullStatusWithVal(BTLEApplication.getApplication().getString(statusResId), val); } - public static CommandResult success() { - return new CommandResult(CommandResult.STATUS_COMLETED, jsonStatusOk()); - } - - public static CommandResult successWithObject(Object object) { - return new CommandResult(CommandResult.STATUS_COMLETED, jsonStatusWithObject(object)); - } - - public static CommandResult failWithStatus(String status) { - return new CommandResult(CommandResult.STATUS_FAILED, jsonStatus(status)); - } - protected CommandResult withStatusAndVal(int statusResId, String val) { return new CommandResult(CommandResult.STATUS_FAILED, jsonFullStatusWithVal(BTLEApplication.getApplication().getString(statusResId), val)); } - public static CommandResult failWithStatus(int strResId) { - return new CommandResult(CommandResult.STATUS_FAILED, jsonStatus(strResId)); - } - - public static CommandResult failTimeoutReached() { - return new CommandResult(CommandResult.STATUS_FAILED, jsonStatusTimeoutReached()); - } - protected CommandResult sucessFull() { return new CommandResult(CommandResult.STATUS_COMLETED, jsonFullStatus(R.string.status_json_success)); } diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/DeviceConnection.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/DeviceConnection.java similarity index 86% rename from app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/DeviceConnection.java rename to app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/DeviceConnection.java index 5ace258..fd608bb 100644 --- a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/DeviceConnection.java +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/DeviceConnection.java @@ -1,7 +1,6 @@ -package com.dataart.btle_android.btle_gateway.gatt.callbacks; +package com.dataart.btle_android.btle_gateway.gatt_callbacks; import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; /** * Created by Constantine Mars on 3/27/15. diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/InteractiveGattCallback.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/InteractiveGattCallback.java similarity index 98% rename from app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/InteractiveGattCallback.java rename to app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/InteractiveGattCallback.java index 6726d9b..9a309b9 100644 --- a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/InteractiveGattCallback.java +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/InteractiveGattCallback.java @@ -1,4 +1,4 @@ -package com.dataart.btle_android.btle_gateway.gatt.callbacks; +package com.dataart.btle_android.btle_gateway.gatt_callbacks; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; @@ -403,11 +403,11 @@ public void call(BluetoothGatt gatt) { // before execute call, we need convert uuids to long format because Android BLE Api understands only last // converstion can't be done in constructor because at that moment services might be not discovered if ((serviceUUID = getFullServiceUuid(serviceUUID)) == null) { - future.call(CmdResult.failWithStatus(context.getString(R.string.status_service_uuid_nf))); + future.call(failWithStatus(context.getString(R.string.status_service_uuid_nf))); return; } if ((characteristicUUID = getFullCharacteristicUuid(characteristicUUID)) == null) { - future.call(CmdResult.failWithStatus(context.getString(R.string.status_char_uuid_nf))); + future.call(failWithStatus(context.getString(R.string.status_char_uuid_nf))); return; } // call @@ -415,7 +415,7 @@ public void call(BluetoothGatt gatt) { try { service = gatt.getService(UUID.fromString(serviceUUID)); } catch (Exception e) { - future.call(CmdResult.failWithStatus("gatt.getService(uuid) crashed: " + e.getMessage())); + future.call(failWithStatus("gatt.getService(uuid) crashed: " + e.getMessage())); return; } diff --git a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/StatusJson.java b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/StatusJson.java similarity index 68% rename from app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/StatusJson.java rename to app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/StatusJson.java index b5e2843..2c07bc1 100644 --- a/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt/callbacks/StatusJson.java +++ b/app/src/main/java/com/dataart/btle_android/btle_gateway/gatt_callbacks/StatusJson.java @@ -1,28 +1,34 @@ -package com.dataart.btle_android.btle_gateway.gatt.callbacks; - -import android.os.Parcel; -import android.os.Parcelable; +package com.dataart.btle_android.btle_gateway.gatt_callbacks; import com.dataart.btle_android.BTLEApplication; import com.dataart.btle_android.R; -import org.apache.commons.codec.binary.Hex; - /** * Created by Constantine Mars on 4/3/15. * * Wraps data for json conversion */ -public abstract class StatusJson { - public static class Status { +abstract class StatusJson { + static String bytes2String(byte[] value) { + String s = ""; + for (byte b : value) { + if (!s.isEmpty()) { + s += ", "; + } + s += String.format("0x%02X", b); + } + return s; + } + + static class Status { private String status; - public Status(String status) { + Status(String status) { this.status = status; } - public static Status statusOk() { - return new Status(BTLEApplication.getApplication().getString(R.string.status_ok)); + static Status statusOk() { + return new Status(BTLEApplication.getApplication().getString(android.R.string.ok)); } public static Status statusOkWithVal(String val) { @@ -30,14 +36,14 @@ public static Status statusOkWithVal(String val) { } public static Status statusFail() { - return new Status(BTLEApplication.getApplication().getString(R.string.status_ok)); + return new Status(BTLEApplication.getApplication().getString(android.R.string.ok)); } public static Status statusFailWithVal(String val) { return new Status(val); } - public static Status statusTimeoutReached() { + static Status statusTimeoutReached() { return new Status(BTLEApplication.getApplication().getString(R.string.status_timeout)); } @@ -46,40 +52,31 @@ public String getStatus() { } } - public static class StatusWithObject { + static class StatusWithObject { private Object result; - public StatusWithObject(Object result) { + StatusWithObject(Object result) { this.result = result; } - public static StatusWithObject statusWithObject(Object object){ + static StatusWithObject statusWithObject(Object object) { return new StatusWithObject(object); } } - public static class FullStatusWithVal extends FullStatus { + static class FullStatusWithVal extends FullStatus { private String value; - public FullStatusWithVal(String status, String device, String serviceUUID, String characteristicUUID, String value) { + FullStatusWithVal(String status, String device, String serviceUUID, String characteristicUUID, String value) { super(status, device, serviceUUID, characteristicUUID); this.value = value; } } - public static class FullStatus { + static class FullStatus { private String status; private String device; private String serviceUUID; - - public String getDevice() { - return device; - } - - public String getStatus() { - return status; - } - private String characteristicUUID; public FullStatus(String status, String device, String serviceUUID, String characteristicUUID) { @@ -88,6 +85,14 @@ public FullStatus(String status, String device, String serviceUUID, String chara this.serviceUUID = serviceUUID; this.characteristicUUID = characteristicUUID; } + + public String getDevice() { + return device; + } + + public String getStatus() { + return status; + } } public static class FullStatusWithByteArray extends FullStatus { @@ -98,15 +103,4 @@ public FullStatusWithByteArray(String status, byte[] value, String device, Strin this.value = value; } } - - public static String bytes2String(byte[] value){ - String s = ""; - for(byte b:value){ - if(!s.isEmpty()){ - s+=", "; - } - s+=String.format("0x%02X",b); - } - return s; - } } \ No newline at end of file diff --git a/app/src/main/java/com/dataart/btle_android/devicehive/BTLEDeviceHive.java b/app/src/main/java/com/dataart/btle_android/devicehive/BTLEDeviceHive.java index 4320e26..82efc36 100644 --- a/app/src/main/java/com/dataart/btle_android/devicehive/BTLEDeviceHive.java +++ b/app/src/main/java/com/dataart/btle_android/devicehive/BTLEDeviceHive.java @@ -12,6 +12,7 @@ import com.dataart.android.devicehive.device.CommandResult; import com.dataart.android.devicehive.device.Device; import com.dataart.android.devicehive.device.future.SimpleCallableFuture; +import com.dataart.btle_android.BuildConfig; import java.util.LinkedList; import java.util.List; @@ -48,7 +49,7 @@ public BTLEDeviceHive(Context context) { private static DeviceData getTestDeviceData() { final Network network = new Network("AndroidBTLE", ""); - final DeviceClass deviceClass = new DeviceClass("AndroidBTLE Device", "1.0"); + final DeviceClass deviceClass = new DeviceClass("Android BTLE Device", BuildConfig.VERSION_NAME); return new DeviceData( new BTLEDevicePreferences().getGatewayId(), diff --git a/app/src/main/java/com/dataart/btle_android/helpers/BleHelpersFactory.java b/app/src/main/java/com/dataart/btle_android/helpers/BleHelpersFactory.java new file mode 100644 index 0000000..fb36f9d --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/BleHelpersFactory.java @@ -0,0 +1,48 @@ +package com.dataart.btle_android.helpers; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.os.Build; + +import com.dataart.btle_android.helpers.ble.base.BleInitializer; +import com.dataart.btle_android.helpers.ble.base.BleScanner; +import com.dataart.btle_android.helpers.ble.initializers.BleInitializerJ; +import com.dataart.btle_android.helpers.ble.initializers.BleInitializerL; +import com.dataart.btle_android.helpers.ble.initializers.BleInitializerM; +import com.dataart.btle_android.helpers.ble.scanners.BleScannerJ; +import com.dataart.btle_android.helpers.ble.scanners.BleScannerL; + +/** + * Created by Constantine Mars on 12/13/15. + * Factory for creating BLE init helper according to OS version + */ +public class BleHelpersFactory { + + public static BleInitializer getInitializer(Activity activity, BleInitializer.InitCompletionCallback initCompletionCallback) { + final int osVersion = Build.VERSION.SDK_INT; + + if (osVersion >= Build.VERSION_CODES.M) { + return new BleInitializerM(activity, initCompletionCallback); + } else if (osVersion >= Build.VERSION_CODES.LOLLIPOP) { + return new BleInitializerL(activity, initCompletionCallback); + } else if (osVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + return new BleInitializerJ(activity, initCompletionCallback); + } + +// There is no BLE support in Android versions below Jelly Bean + return null; + } + + public static BleScanner getScanner(BleScanner.ScanCallback scanCallback, BluetoothAdapter bluetoothAdapter) { + final int osVersion = Build.VERSION.SDK_INT; + + if (osVersion >= Build.VERSION_CODES.LOLLIPOP) { + return new BleScannerL(scanCallback, bluetoothAdapter); + } else if (osVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + return new BleScannerJ(scanCallback, bluetoothAdapter); + } + +// There is no BLE support in Android versions below Jelly Bean + return null; + } +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/LocationHelper.java b/app/src/main/java/com/dataart/btle_android/helpers/LocationHelper.java new file mode 100644 index 0000000..11f6309 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/LocationHelper.java @@ -0,0 +1,209 @@ +package com.dataart.btle_android.helpers; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.location.Location; +import android.location.LocationManager; +import android.os.Bundle; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.dataart.btle_android.R; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationSettingsResult; +import com.google.android.gms.location.LocationSettingsStatusCodes; + +import timber.log.Timber; + +/** + * Created by Constantine Mars on 12/6/15. + * Android M with Google Play Services requires Location enabled before starting BLE devices discovery + * This helper implements turning on Location services programmatically + */ +public class LocationHelper implements ResultCallback, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { + private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000; + private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = + UPDATE_INTERVAL_IN_MILLISECONDS / 2; + private static final int REQUEST_CHECK_SETTINGS = 100; + private LocationSettingsRequest mLocationSettingsRequest; + private GoogleApiClient mGoogleApiClient; + private LocationRequest mLocationRequest; + private LocationEnabledListener listener; + private Activity activity; + private boolean waitForResume = false; + + public LocationHelper(LocationEnabledListener listener, Activity activity) { + this.listener = listener; + this.activity = activity; + buildGoogleApiClient(); + createLocationRequest(); + buildLocationSettingsRequest(); + } + + public void onStart() { + mGoogleApiClient.connect(); + } + + public void onStop() { + mGoogleApiClient.disconnect(); + } + + /** + * Build request to enable Location + */ + private void buildLocationSettingsRequest() { + LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder(); + builder.addLocationRequest(mLocationRequest); + mLocationSettingsRequest = builder.build(); + } + + private void buildGoogleApiClient() { + mGoogleApiClient = new GoogleApiClient.Builder(activity) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .addApi(LocationServices.API) + .build(); + } + + /** + * Location request is necessary for defining which exactly permissions must be enabled + */ + protected void createLocationRequest() { + mLocationRequest = new LocationRequest(); + mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS); + mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS); + mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + } + + private void doJob() { + listener.onLocationEnabled(); + } + + @Override + public void onResult(@NonNull LocationSettingsResult locationSettingsResult) { + final Status status = locationSettingsResult.getStatus(); + switch (status.getStatusCode()) { + case LocationSettingsStatusCodes.SUCCESS: + Timber.i(activity.getString(R.string.location_settings_ok)); + doJob(); + break; + case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: + Timber.e(activity.getString(R.string.location_fail)); + + try { + // Show the dialog by calling startResolutionForResult(), and check the result + // in onActivityResult(). + status.startResolutionForResult(activity, REQUEST_CHECK_SETTINGS); + } catch (IntentSender.SendIntentException e) { + Timber.e(activity.getString(R.string.pi_fail)); + } + break; + case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: + Timber.e(activity.getString(R.string.loc_settings_inadequate)); + break; + } + } + + @Override + public void onConnected(@Nullable Bundle bundle) { + + } + + @Override + public void onConnectionSuspended(int i) { + + } + + @Override + public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { + + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + // Check for the integer request code originally supplied to startResolutionForResult(). + case REQUEST_CHECK_SETTINGS: + switch (resultCode) { + case Activity.RESULT_OK: + Timber.i(activity.getString(R.string.location_ok)); + doJob(); + break; + case Activity.RESULT_CANCELED: + Timber.e(activity.getString(R.string.location_cancelled)); + break; + } + break; + } + } + + private boolean isLocationEnabled() { + LocationManager lm = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE); + boolean gps_enabled = false; + boolean network_enabled = false; + + try { + gps_enabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER); + } catch (Exception ignored) { + } + + try { + network_enabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER); + } catch (Exception ignored) { + } + + return gps_enabled || network_enabled; + } + + public void checkLocationEnabled() { + if (isLocationEnabled()) { + doJob(); + return; + } + +// If location isn't enabled - check whether we can call Google Play Services or user to manually +// switch Location + final int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(activity.getApplicationContext()); + if (status == ConnectionResult.SUCCESS) { + PendingResult result = + LocationServices.SettingsApi.checkLocationSettings( + mGoogleApiClient, + mLocationSettingsRequest + ); + result.setResultCallback(this); + } else { +// If no services available - the only thing we can do is to +// ask user to switch Location manually + activity.startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + waitForResume = true; + } + } + + public void onResume() { + if (waitForResume) { + checkLocationEnabled(); + } + } + + @Override + public void onLocationChanged(Location location) { + + } + + public interface LocationEnabledListener { + void onLocationEnabled(); + } + +} + diff --git a/app/src/main/java/com/dataart/btle_android/helpers/PermissionsHelper.java b/app/src/main/java/com/dataart/btle_android/helpers/PermissionsHelper.java new file mode 100644 index 0000000..f64feb8 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/PermissionsHelper.java @@ -0,0 +1,66 @@ +package com.dataart.btle_android.helpers; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.pm.PackageManager; +import android.os.Build; + +import com.dataart.btle_android.R; + +import java.util.ArrayList; +import java.util.List; + +import rx.functions.Action1; + +/** + * Created by Constantine Mars on 12/15/15. + */ +@TargetApi(Build.VERSION_CODES.M) +public class PermissionsHelper { + private static final int PERMISSION_REQUEST = 2001; + + public static boolean checkPermissions(Activity activity, String[] permissions, String message) { + List permissionsToCheck = new ArrayList<>(); + rx.Observable.from(permissions) + .filter(permission -> activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) + .forEach(permissionsToCheck::add); + + if (permissionsToCheck.isEmpty()) { + return true; + } + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.needs_permissions); + builder.setMessage(message); + builder.setPositiveButton(android.R.string.ok, null); + builder.setOnDismissListener(dialog -> activity.requestPermissions( + permissionsToCheck.toArray( + new String[permissionsToCheck.size()] + ), PERMISSION_REQUEST)); + builder.show(); + + return false; + } + + public static void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults, + Action1 onSuccess, Action1 onError) { + switch (requestCode) { + case PERMISSION_REQUEST: { + List permissionsNotGranted = new ArrayList<>(); + + for (String permission : permissions) { + if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + permissionsNotGranted.add(permission); + } + } + + if (permissionsNotGranted.size() > 0) { + onError.call(permissionsNotGranted.toArray(new String[permissionsNotGranted.size()])); + } else { + onSuccess.call("All required permissions granted"); + } + } + } + } +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/ble/base/BleInitializer.java b/app/src/main/java/com/dataart/btle_android/helpers/ble/base/BleInitializer.java new file mode 100644 index 0000000..3564555 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/ble/base/BleInitializer.java @@ -0,0 +1,97 @@ +package com.dataart.btle_android.helpers.ble.base; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.content.Intent; + +import com.dataart.btle_android.R; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +/** + * Created by Constantine Mars on 12/13/15. + *

+ * Base BLE init helper logic, common for all Android versions, starting with JellyBean MR2 + */ +@Data +@RequiredArgsConstructor +public abstract class BleInitializer { + private static final int REQUEST_ENABLE_BT = 3001; + + protected final Activity activity; + protected final InitCompletionCallback initCompletionCallback; + protected BluetoothAdapter bluetoothAdapter; + /** + * Enabling Bluetooth should be always the last step of initialization, + * after which initCompletionCallback will be called + */ + private boolean declined = false; + + /** + * Init delivers BluetoothAdapter for all platform-specific versions of initializer + */ + protected void init() { + final BluetoothManager bluetoothManager = (BluetoothManager) activity.getSystemService(Context.BLUETOOTH_SERVICE); + bluetoothAdapter = bluetoothManager.getAdapter(); + } + + /** + * Start must be called before beginning scanning or any other opeartions on bluetooth + */ + public abstract void start(); + + public boolean enableBluetooth() { + if (bluetoothAdapter == null) { + Timber.e(activity.getString(R.string.bt_not_supported)); + return false; + } + + if (!bluetoothAdapter.isEnabled() && !declined) { + Timber.i(activity.getString(R.string.bt_enabling_started)); + + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); + return false; + } + + Timber.i(activity.getString(R.string.bt_enabled)); + initCompletionCallback.onInitCompleted(bluetoothAdapter); + return true; + } + + /** + * Need to be called from Activity onActivityResult + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_ENABLE_BT: + if (resultCode == Activity.RESULT_OK) { + start(); + } else { + declined = false; + } + break; + } + } + + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + } + + public void onStart() { + } + + public void onStop() { + } + + public void onResume() { + } + + + public interface InitCompletionCallback { + void onInitCompleted(BluetoothAdapter bluetoothAdapter); + } +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/ble/base/BleScanner.java b/app/src/main/java/com/dataart/btle_android/helpers/ble/base/BleScanner.java new file mode 100644 index 0000000..a5a1963 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/ble/base/BleScanner.java @@ -0,0 +1,27 @@ +package com.dataart.btle_android.helpers.ble.base; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +/** + * Created by Constantine Mars on 6/13/16. + * Helper for BLE scanner. Can be singleton and accessible from android services + */ + +@Data +@RequiredArgsConstructor +public abstract class BleScanner { + protected final ScanCallback scanCallback; + protected final BluetoothAdapter bluetoothAdapter; + + public abstract void startScan(); + + public abstract void stopScan(); + + public interface ScanCallback { + void onDeviceFound(BluetoothDevice device, int rssi, byte[] scanRecord); + } +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerJ.java b/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerJ.java new file mode 100644 index 0000000..87c66a4 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerJ.java @@ -0,0 +1,30 @@ +package com.dataart.btle_android.helpers.ble.initializers; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.os.Build; + +import com.dataart.btle_android.helpers.ble.base.BleInitializer; + +import lombok.Data; + +/** + * Created by Constantine Mars on 12/13/15. + *

+ * Jelly Bean MR2 - specific BLE helper + */ +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +@Data +public class BleInitializerJ extends BleInitializer { + + public BleInitializerJ(Activity activity, InitCompletionCallback initCompletionCallback) { + super(activity, initCompletionCallback); + + init(); + } + + @Override + public void start() { + enableBluetooth(); + } +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerL.java b/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerL.java new file mode 100644 index 0000000..46a2186 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerL.java @@ -0,0 +1,22 @@ +package com.dataart.btle_android.helpers.ble.initializers; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.os.Build; + +import lombok.Data; + +/** + * Created by Constantine Mars on 12/6/15. + *

+ * Lollipop-specific BLE Helper + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +@Data +public class BleInitializerL extends BleInitializerJ { + + public BleInitializerL(Activity activity, InitCompletionCallback initCompletionCallback) { + super(activity, initCompletionCallback); + } + +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerM.java b/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerM.java new file mode 100644 index 0000000..62ee440 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/ble/initializers/BleInitializerM.java @@ -0,0 +1,116 @@ +package com.dataart.btle_android.helpers.ble.initializers; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.os.Build; + +import com.dataart.btle_android.R; +import com.dataart.btle_android.helpers.LocationHelper; +import com.dataart.btle_android.helpers.PermissionsHelper; +import com.dataart.btle_android.helpers.ble.base.BleInitializer; + +import rx.functions.Action1; +import timber.log.Timber; + +/** + * Created by Constantine Mars on 12/13/15. + *

+ * Marshmallow-specific BLE helper + */ +public class BleInitializerM extends BleInitializerL { + private static String[] permissions = new String[]{ + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.ACCESS_FINE_LOCATION + }; + private Action1 onError; + private Action1 onSuccess; + private LocationHelper locationHelper; + + public BleInitializerM(Activity activity, BleInitializer.InitCompletionCallback initCompletionCallback) { + super(activity, initCompletionCallback); + locationHelper = new LocationHelper(this::callSuperStart, activity); + + onError = permissions -> { + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.no_permissions_title); + builder.setMessage(R.string.no_permissions_message); + builder.setPositiveButton(android.R.string.ok, null); + builder.setOnDismissListener(null); + builder.show(); + }; + onSuccess = s -> { + enableLocation(); + Timber.i(s); + }; + } + + /** + * 1. Request permissions at runtime (Android M specific) + */ + @Override + public void start() { + requestPermissions(); + } + + /** + * 2. Enable location (switch location on if it's still not on) + */ + private void enableLocation() { + locationHelper.checkLocationEnabled(); + } + + /** + * 3. Call super.start() to complete sequence (enable bluetooth and call initCompletionCallback.onComplete()) + */ + private void callSuperStart() { + super.start(); + } + + public void requestPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (PermissionsHelper.checkPermissions( + activity, + permissions, + activity.getString(R.string.permissions_explanation))) { + enableLocation(); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + PermissionsHelper.onRequestPermissionsResult( + requestCode, + permissions, + grantResults, + onSuccess, + onError); + } + + @Override + public void onStart() { + super.onStart(); + locationHelper.onStart(); + } + + @Override + public void onStop() { + locationHelper.onStop(); + super.onStop(); + } + + @Override + public void onResume() { + super.onResume(); + locationHelper.onResume(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + locationHelper.onActivityResult(requestCode, resultCode, data); + super.onActivityResult(requestCode, resultCode, data); + } +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/ble/scanners/BleScannerJ.java b/app/src/main/java/com/dataart/btle_android/helpers/ble/scanners/BleScannerJ.java new file mode 100644 index 0000000..d775996 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/ble/scanners/BleScannerJ.java @@ -0,0 +1,32 @@ +package com.dataart.btle_android.helpers.ble.scanners; + +import android.bluetooth.BluetoothAdapter; + +import com.dataart.btle_android.helpers.ble.base.BleScanner; + + +/** + * Created by Constantine Mars on 6/13/16. + * Scanner for Android Jelly Bean + */ + +@SuppressWarnings("deprecation") +public class BleScannerJ extends BleScanner { + + private final BluetoothAdapter.LeScanCallback callback; + + public BleScannerJ(ScanCallback scanCallback, BluetoothAdapter bluetoothAdapter) { + super(scanCallback, bluetoothAdapter); + callback = scanCallback::onDeviceFound; + } + + @Override + public void startScan() { + bluetoothAdapter.startLeScan(callback); + } + + @Override + public void stopScan() { + bluetoothAdapter.stopLeScan(callback); + } +} diff --git a/app/src/main/java/com/dataart/btle_android/helpers/ble/scanners/BleScannerL.java b/app/src/main/java/com/dataart/btle_android/helpers/ble/scanners/BleScannerL.java new file mode 100644 index 0000000..514a312 --- /dev/null +++ b/app/src/main/java/com/dataart/btle_android/helpers/ble/scanners/BleScannerL.java @@ -0,0 +1,74 @@ +package com.dataart.btle_android.helpers.ble.scanners; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanResult; +import android.os.Build; + +import java.util.List; + +/** + * Created by Constantine Mars on 6/13/16. + *

+ * Scanner for Android L and above + */ + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class BleScannerL extends BleScannerJ { + + private BluetoothLeScanner scanner; + + private android.bluetooth.le.ScanCallback callback; + + public BleScannerL(ScanCallback scanCallback, BluetoothAdapter bluetoothAdapter) { + super(scanCallback, bluetoothAdapter); + + callback = createCallback(scanCallback); + } + + private static android.bluetooth.le.ScanCallback createCallback(ScanCallback scanCallback) { + return new android.bluetooth.le.ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + + BluetoothDevice device = result.getDevice(); + int rssi = result.getRssi(); + byte[] scanRecord = (result.getScanRecord() != null ? result.getScanRecord().getBytes() : null); + scanCallback.onDeviceFound(device, rssi, scanRecord); + + super.onScanResult(callbackType, result); + } + + @Override + public void onBatchScanResults(List results) { + super.onBatchScanResults(results); + } + + @Override + public void onScanFailed(int errorCode) { + super.onScanFailed(errorCode); + } + }; + } + + private BluetoothLeScanner getScanner() { + if (scanner == null && bluetoothAdapter != null) { + scanner = bluetoothAdapter.getBluetoothLeScanner(); + } + return scanner; + } + + @Override + public void startScan() { + getScanner().startScan(callback); + } + + @Override + public void stopScan() { + getScanner().stopScan(callback); + } + + +} diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 7a1806b..a65cbad 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,106 +1,134 @@ - + android:layout_height="match_parent"> - - - - - - - - - - - - + + - - + android:layout_height="match_parent" + android:layout_below="@+id/toolbar"> - - - - - - - - - - - - - - - - -