Modernization of modular approach

This commit is contained in:
Sylwester Zieliński
2021-09-24 10:25:10 +02:00
parent 91b0e39f8e
commit 3ef57bf5fd
52 changed files with 943 additions and 964 deletions

18
lib_service/build.gradle Normal file
View File

@@ -0,0 +1,18 @@
apply from: rootProject.file("library.gradle")
apply plugin: 'kotlin-parcelize'
dependencies {
implementation project(":feature_scanner")
implementation libs.nordic.ble.common
implementation libs.nordic.log
implementation libs.lifecycle.service
implementation libs.localbroadcastmanager
testImplementation libs.test.junit
androidTestImplementation libs.android.test.junit
androidTestImplementation libs.android.test.espresso
androidTestImplementation libs.android.test.compose.ui
debugImplementation libs.android.test.compose.tooling
}

View File

@@ -0,0 +1,24 @@
package no.nordicsemi.android.service
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("no.nordicsemi.android.service.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="no.nordicsemi.android.service">
</manifest>

View File

@@ -0,0 +1,130 @@
package no.nordicsemi.android.service
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import androidx.annotation.IntRange
import no.nordicsemi.android.ble.callback.DataReceivedCallback
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.log.LogContract
import java.util.*
/**
* The Ble Manager with Battery Service support.
*
* @param <T> The profile callbacks type.
* @see BleManager
</T> */
abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) : LoggableBleManager<T>(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
/**
* Returns the last received Battery Level value.
* The value is set to null when the device disconnects.
* @return Battery Level value, in percent.
*/
/** Last received Battery Level value. */
var batteryLevel: Int? = null
private set
private val batteryLevelDataCallback: DataReceivedCallback =
object : BatteryLevelDataCallback() {
override fun onBatteryLevelChanged(
device: BluetoothDevice,
@IntRange(from = 0, to = 100) batteryLevel: Int
) {
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
this@BatteryManager.batteryLevel = batteryLevel
mCallbacks?.onBatteryLevelChanged(device, batteryLevel)
}
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
log(Log.WARN, "Invalid Battery Level data received: $data")
}
}
fun readBatteryLevelCharacteristic() {
if (isConnected) {
readCharacteristic(batteryLevelCharacteristic)
.with(batteryLevelDataCallback)
.fail { device: BluetoothDevice?, status: Int ->
log(
Log.WARN,
"Battery Level characteristic not found"
)
}
.enqueue()
}
}
fun enableBatteryLevelCharacteristicNotifications() {
if (isConnected) {
// If the Battery Level characteristic is null, the request will be ignored
setNotificationCallback(batteryLevelCharacteristic)
.with(batteryLevelDataCallback)
enableNotifications(batteryLevelCharacteristic)
.done { device: BluetoothDevice? ->
log(
Log.INFO,
"Battery Level notifications enabled"
)
}
.fail { device: BluetoothDevice?, status: Int ->
log(
Log.WARN,
"Battery Level characteristic not found"
)
}
.enqueue()
}
}
/**
* Disables Battery Level notifications on the Server.
*/
fun disableBatteryLevelCharacteristicNotifications() {
if (isConnected) {
disableNotifications(batteryLevelCharacteristic)
.done { device: BluetoothDevice? ->
log(
Log.INFO,
"Battery Level notifications disabled"
)
}
.enqueue()
}
}
protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
readBatteryLevelCharacteristic()
enableBatteryLevelCharacteristicNotifications()
}
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(BATTERY_SERVICE_UUID)
if (service != null) {
batteryLevelCharacteristic = service.getCharacteristic(
BATTERY_LEVEL_CHARACTERISTIC_UUID
)
}
return batteryLevelCharacteristic != null
}
override fun onDeviceDisconnected() {
batteryLevelCharacteristic = null
batteryLevel = null
}
}
companion object {
/** Battery Service UUID. */
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
/** Battery Level characteristic UUID. */
private val BATTERY_LEVEL_CHARACTERISTIC_UUID =
UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
}
}

View File

@@ -0,0 +1,6 @@
package no.nordicsemi.android.service
import no.nordicsemi.android.ble.BleManagerCallbacks
import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback
interface BatteryManagerCallbacks : BleManagerCallbacks, BatteryLevelCallback

View File

@@ -0,0 +1,554 @@
/*
* Copyright (c) 2015, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Binder
import android.os.Handler
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.lifecycle.LifecycleService
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint
import no.nordicsemi.android.ble.BleManagerCallbacks
import no.nordicsemi.android.ble.utils.ILogger
import no.nordicsemi.android.log.ILogSession
import no.nordicsemi.android.log.Logger
import no.nordicsemi.android.scanner.tools.SelectedBluetoothDeviceHolder
import javax.inject.Inject
@AndroidEntryPoint
abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
private var bleManager: LoggableBleManager<out BleManagerCallbacks>? = null
@Inject
lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder
/**
* Returns a handler that is created in onCreate().
* The handler may be used to postpone execution of some operations or to run them in UI thread.
*/
protected var handler: Handler? = null
private set
protected var bound = false
private var activityIsChangingConfiguration = false
/**
* Returns the Bluetooth device object
*
* @return bluetooth device
*/
protected val bluetoothDevice: BluetoothDevice by lazy {
bluetoothDeviceHolder.device ?: throw UnsupportedOperationException(
"No device address at EXTRA_DEVICE_ADDRESS key"
)
}
/**
* Returns the device name
*
* @return the device name
*/
protected var deviceName: String? = null
private set
/**
* Returns the log session that can be used to append log entries. The method returns `null` if the nRF Logger app was not installed. It is safe to use logger when
* [.onServiceStarted] has been called.
*
* @return the log session
*/
protected var logSession: ILogSession? = null
private set
private val bluetoothStateBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF)
val logger: ILogger = binder
val stateString =
"[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(
state
)
logger.log(Log.DEBUG, stateString)
when (state) {
BluetoothAdapter.STATE_ON -> onBluetoothEnabled()
BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> onBluetoothDisabled()
}
}
private fun state2String(state: Int): String {
return when (state) {
BluetoothAdapter.STATE_TURNING_ON -> "TURNING ON"
BluetoothAdapter.STATE_ON -> "ON"
BluetoothAdapter.STATE_TURNING_OFF -> "TURNING OFF"
BluetoothAdapter.STATE_OFF -> "OFF"
else -> "UNKNOWN ($state)"
}
}
}
inner class LocalBinder : Binder(), ILogger {
/**
* Disconnects from the sensor.
*/
fun disconnect() {
val state = bleManager!!.connectionState
if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) {
bleManager!!.close()
onDeviceDisconnected(bluetoothDevice!!)
return
}
bleManager!!.disconnect().enqueue()
}
/**
* Sets whether the bound activity if changing configuration or not.
* If `false`, we will turn off battery level notifications in onUnbind(..) method below.
*
* @param changing true if the bound activity is finishing
*/
fun setActivityIsChangingConfiguration(changing: Boolean) {
activityIsChangingConfiguration = changing
}
/**
* Returns the device address
*
* @return device address
*/
val deviceAddress: String
get() = bluetoothDevice!!.address
/**
* Returns the device name
*
* @return the device name
*/
fun getDeviceName(): String? {
return deviceName
}
/**
* Returns the Bluetooth device
*
* @return the Bluetooth device
*/
fun getBluetoothDevice(): BluetoothDevice? {
return bluetoothDevice
}
/**
* Returns `true` if the device is connected to the sensor.
*
* @return `true` if device is connected to the sensor, `false` otherwise
*/
val isConnected: Boolean
get() = bleManager!!.isConnected
/**
* Returns the connection state of given device.
*
* @return the connection state, as in [BleManager.getConnectionState].
*/
val connectionState: Int
get() = bleManager!!.connectionState
/**
* Returns the log session that can be used to append log entries.
* The log session is created when the service is being created.
* The method returns `null` if the nRF Logger app was not installed.
*
* @return the log session
*/
fun getLogSession(): ILogSession? {
return logSession
}
override fun log(level: Int, message: String) {
Logger.log(logSession, level, message)
}
override fun log(level: Int, @StringRes messageRes: Int, vararg params: Any) {
Logger.log(logSession, level, messageRes, *params)
}
}// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
/**
* Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity.
*
* @return the service binder
*/
protected val binder: LocalBinder
protected get() =// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
LocalBinder()
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
bound = true
return binder
}
override fun onRebind(intent: Intent) {
bound = true
if (!activityIsChangingConfiguration) onRebind()
}
/**
* Called when the activity has rebound to the service after being recreated.
* This method is not called when the activity was killed to be recreated when the phone orientation changed
* if prior to being killed called [LocalBinder.setActivityIsChangingConfiguration] with parameter true.
*/
protected open fun onRebind() {
// empty default implementation
}
override fun onUnbind(intent: Intent): Boolean {
bound = false
if (!activityIsChangingConfiguration) onUnbind()
// We want the onRebind method be called if anything else binds to it again
return true
}
/**
* Called when the activity has unbound from the service before being finished.
* This method is not called when the activity is killed to be recreated when the phone orientation changed.
*/
protected open fun onUnbind() {
// empty default implementation
}
override fun onCreate() {
super.onCreate()
handler = Handler()
// Initialize the manager
bleManager = initializeManager()
// Register broadcast receivers
registerReceiver(
bluetoothStateBroadcastReceiver,
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
)
// Service has now been created
onServiceCreated()
// Call onBluetoothEnabled if Bluetooth enabled
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter.isEnabled) {
onBluetoothEnabled()
}
}
/**
* Called when the service has been created, before the [.onBluetoothEnabled] is called.
*/
protected fun onServiceCreated() {
// empty default implementation
}
/**
* Initializes the Ble Manager responsible for connecting to a single device.
*
* @return a new BleManager object
*/
protected abstract fun initializeManager(): LoggableBleManager<out BleManagerCallbacks>
/**
* This method returns whether autoConnect option should be used.
*
* @return true to use autoConnect feature, false (default) otherwise.
*/
protected fun shouldAutoConnect(): Boolean {
return false
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val logUri = intent?.getParcelableExtra<Uri>(EXTRA_LOG_URI)
logSession = Logger.openSession(applicationContext, logUri)
deviceName = intent?.getStringExtra(EXTRA_DEVICE_NAME)
Logger.i(logSession, "Service started")
val adapter = BluetoothAdapter.getDefaultAdapter()
bleManager!!.setLogger(logSession)
onServiceStarted()
bleManager!!.connect(bluetoothDevice)
.useAutoConnect(shouldAutoConnect())
.retry(3, 100)
.enqueue()
return START_REDELIVER_INTENT
}
/**
* Called when the service has been started. The device name and address are set.
* The BLE Manager will try to connect to the device after this method finishes.
*/
protected fun onServiceStarted() {
// empty default implementation
}
override fun onTaskRemoved(rootIntent: Intent) {
super.onTaskRemoved(rootIntent)
// This method is called when user removed the app from Recents.
// By default, the service will be killed and recreated immediately after that.
// However, all managed devices will be lost and devices will be disconnected.
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
// Unregister broadcast receivers
unregisterReceiver(bluetoothStateBroadcastReceiver)
// shutdown the manager
bleManager!!.close()
Logger.i(logSession, "Service destroyed")
bleManager = null
bluetoothDeviceHolder.forgetDevice()
deviceName = null
logSession = null
handler = null
}
/**
* Method called when Bluetooth Adapter has been disabled.
*/
protected fun onBluetoothDisabled() {
// empty default implementation
}
/**
* This method is called when Bluetooth Adapter has been enabled and
* after the service was created if Bluetooth Adapter was enabled at that moment.
* This method could initialize all Bluetooth related features, for example open the GATT server.
*/
protected fun onBluetoothEnabled() {
// empty default implementation
}
override fun onDeviceConnecting(device: BluetoothDevice) {
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onDeviceConnected(device: BluetoothDevice) {
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_DEVICE_NAME, deviceName)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onDeviceDisconnecting(device: BluetoothDevice) {
// Notify user about changing the state to DISCONNECTING
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
/**
* This method should return false if the service needs to do some asynchronous work after if has disconnected from the device.
* In that case the [.stopService] method must be called when done.
*
* @return true (default) to automatically stop the service when device is disconnected. False otherwise.
*/
protected fun stopWhenDisconnected(): Boolean {
return true
}
override fun onDeviceDisconnected(device: BluetoothDevice) {
// Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above
// Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be
// invoked ONLY when user requested disconnection (using Disconnect button). If the device
// disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead.
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
if (stopWhenDisconnected()) stopService()
}
protected fun stopService() {
// user requested disconnection. We must stop the service
Logger.v(logSession, "Stopping service...")
stopSelf()
}
override fun onLinkLossOccurred(device: BluetoothDevice) {
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onServicesDiscovered(device: BluetoothDevice, optionalServicesFound: Boolean) {
val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true)
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onDeviceReady(device: BluetoothDevice) {
val broadcast = Intent(BROADCAST_DEVICE_READY)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onDeviceNotSupported(device: BluetoothDevice) {
val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false)
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
// no need for disconnecting, it will be disconnected by the manager automatically
}
override fun onBatteryValueReceived(device: BluetoothDevice, value: Int) {
val broadcast = Intent(BROADCAST_BATTERY_LEVEL)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_BATTERY_LEVEL, value)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onBondingRequired(device: BluetoothDevice) {
showToast(R.string.csc_bonding)
val broadcast = Intent(BROADCAST_BOND_STATE)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onBonded(device: BluetoothDevice) {
showToast(R.string.csc_bonded)
val broadcast = Intent(BROADCAST_BOND_STATE)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onBondingFailed(device: BluetoothDevice) {
showToast(R.string.csc_bonding_failed)
val broadcast = Intent(BROADCAST_BOND_STATE)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
override fun onError(device: BluetoothDevice, message: String, errorCode: Int) {
val broadcast = Intent(BROADCAST_ERROR)
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
broadcast.putExtra(EXTRA_ERROR_MESSAGE, message)
broadcast.putExtra(EXTRA_ERROR_CODE, errorCode)
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param messageResId an resource id of the message to be shown
*/
protected fun showToast(messageResId: Int) {
handler!!.post {
Toast.makeText(this@BleProfileService, messageResId, Toast.LENGTH_SHORT).show()
}
}
/**
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
*
* @param message a message to be shown
*/
protected fun showToast(message: String?) {
handler!!.post {
Toast.makeText(this@BleProfileService, message, Toast.LENGTH_SHORT).show()
}
}
/**
* Returns the device address
*
* @return device address
*/
protected val deviceAddress: String
protected get() = bluetoothDevice!!.address
/**
* Returns `true` if the device is connected to the sensor.
*
* @return `true` if device is connected to the sensor, `false` otherwise
*/
protected val isConnected: Boolean
protected get() = bleManager != null && bleManager!!.isConnected
companion object {
private const val TAG = "BleProfileService"
const val BROADCAST_CONNECTION_STATE =
"no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE"
const val BROADCAST_SERVICES_DISCOVERED =
"no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED"
const val BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY"
const val BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE"
@Deprecated("")
val BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"
const val BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR"
/**
* The key for the device name that is returned in [.BROADCAST_CONNECTION_STATE] with state [.STATE_CONNECTED].
*/
const val EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME"
const val EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"
const val EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI"
const val EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE"
const val EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE"
const val EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY"
const val EXTRA_SERVICE_SECONDARY =
"no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY"
@Deprecated("")
val EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"
const val EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE"
const val EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE"
const val STATE_LINK_LOSS = -1
const val STATE_DISCONNECTED = 0
const val STATE_CONNECTED = 1
const val STATE_CONNECTING = 2
const val STATE_DISCONNECTING = 3
}
}

View File

@@ -0,0 +1,19 @@
package no.nordicsemi.android.service
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
abstract class BluetoothDataReadBroadcast<T> {
private val _event = MutableSharedFlow<T>(
replay = 1,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<T> = _event
fun offer(newEvent: T) {
_event.tryEmit(newEvent)
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (c) 2015, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.service
import android.app.Notification
import android.app.NotificationManager
import android.os.Build
abstract class ForegroundBleService<T : BatteryManager<out BatteryManagerCallbacks>> : BleProfileService() {
protected abstract val manager: T
override fun onDestroy() {
// when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService
cancelNotification()
super.onDestroy()
}
override fun onRebind() {
stopForegroundService()
if (isConnected) {
// This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property).
// If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications.
manager.readBatteryLevelCharacteristic()
}
}
override fun onUnbind() {
// When we are connected, but the application is not open, we are not really interested in battery level notifications.
// But we will still be receiving other values, if enabled.
if (isConnected) manager.disableBatteryLevelCharacteristicNotifications()
startForegroundService()
}
/**
* Sets the service as a foreground service
*/
private fun startForegroundService() {
// when the activity closes we need to show the notification that user is connected to the peripheral sensor
// We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services
val notification = createNotification(R.string.csc_notification_connected_message, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForeground(NOTIFICATION_ID, notification)
} else {
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
nm.notify(NOTIFICATION_ID, notification)
}
}
/**
* Stops the service as a foreground service
*/
private fun stopForegroundService() {
// when the activity rebinds to the service, remove the notification and stop the foreground service
// on devices running Android 8.0 (Oreo) or above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true)
} else {
cancelNotification()
}
}
/**
* Creates the notification
*
* @param messageResId the message resource id. The message must have one String parameter,<br></br>
* f.e. `<string name="name">%s is connected</string>`
* @param defaults
*/
private fun createNotification(messageResId: Int, defaults: Int): Notification {
TODO()
// final Intent parentIntent = new Intent(this, FeaturesActivity.class);
// parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// final Intent targetIntent = new Intent(this, CSCActivity.class);
//
// final Intent disconnect = new Intent(ACTION_DISCONNECT);
// final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT);
//
// // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed
// final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT);
// final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL);
// builder.setContentIntent(pendingIntent);
// builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName()));
// builder.setSmallIcon(R.drawable.ic_stat_notify_csc);
// builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true);
// builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction));
//
// return builder.build();
}
/**
* Cancels the existing notification. If there is no active notification this method does nothing
*/
private fun cancelNotification() {
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
nm.cancel(NOTIFICATION_ID)
}
companion object {
private const val NOTIFICATION_ID = 200
}
}

View File

@@ -0,0 +1,32 @@
package no.nordicsemi.android.service
import android.content.Context
import android.util.Log
import no.nordicsemi.android.ble.BleManagerCallbacks
import no.nordicsemi.android.ble.LegacyBleManager
import no.nordicsemi.android.log.ILogSession
import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.log.Logger
/**
* The manager that logs to nRF Logger. If nRF Logger is not installed, logs are ignored.
*
* @param <T> the callbacks class.
</T> */
abstract class LoggableBleManager<T : BleManagerCallbacks?>(context: Context) : LegacyBleManager<T>(context) {
private var logSession: ILogSession? = null
/**
* Sets the log session to log into.
*
* @param session nRF Logger log session to log inti, or null, if nRF Logger is not installed.
*/
fun setLogger(session: ILogSession?) {
logSession = session
}
override fun log(priority: Int, message: String) {
Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message)
Log.println(priority, "BleManager", message)
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="csc_bonding_failed">Bonding failed.</string>
<string name="csc_bonding">Bonding with the device&#8230;</string>
<string name="csc_bonded">The device is now bonded.</string>
<string name="csc_notification_connected_message">%s is connected.</string>
</resources>

View File

@@ -0,0 +1,17 @@
package no.nordicsemi.android.service
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}