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

View File

@@ -51,10 +51,8 @@ dependencies {
//Hilt requires to implement every module in the main app module
//https://github.com/google/dagger/issues/2123
implementation project(":feature_csc")
implementation project(":lib_broadcast")
implementation project(":lib_events")
implementation project(":lib_theme")
implementation project(":lib_scanner")
implementation project(':feature_scanner')
implementation libs.nordic.ble.common

View File

@@ -4,7 +4,7 @@ buildscript {
compose_version = '1.1.0-alpha03'
kotlin_version = '1.5.30'
android_api_version = 31
android_min_api_version = 21
android_min_api_version = 26
}
repositories {
google()

View File

@@ -2,10 +2,10 @@ apply from: rootProject.file("library.gradle")
apply plugin: 'kotlin-parcelize'
dependencies {
implementation project(":lib_broadcast")
implementation project(":lib_events")
implementation project(":lib_service")
implementation project(":lib_theme")
implementation project(":lib_scanner")
implementation project(':feature_scanner')
implementation project(":lib_utils")
implementation libs.nordic.ble.common

View File

@@ -1,10 +1,10 @@
package no.nordicsemi.android.events
package no.nordicsemi.android.csc.events
import android.bluetooth.BluetoothDevice
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed class CSCServiceEvent : BluetoothReadDataEvent(), Parcelable
sealed class CSCServiceEvent : Parcelable
@Parcelize
data class OnDistanceChangedEvent(

View File

@@ -1,613 +0,0 @@
/*
* 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.csc.service;
import android.app.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.NonNull;
import androidx.annotation.StringRes;
import androidx.lifecycle.LifecycleService;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.BleManagerCallbacks;
import no.nordicsemi.android.ble.utils.ILogger;
import no.nordicsemi.android.csc.R;
import no.nordicsemi.android.csc.batery.LoggableBleManager;
import no.nordicsemi.android.log.ILogSession;
import no.nordicsemi.android.log.Logger;
@SuppressWarnings("unused")
public abstract class BleProfileService extends LifecycleService implements BleManagerCallbacks {
@SuppressWarnings("unused")
private static final String TAG = "BleProfileService";
public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE";
public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED";
public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY";
public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE";
@Deprecated
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR";
/**
* The parameter passed when creating the service. Must contain the address of the sensor that we want to connect to
*/
public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS";
/**
* The key for the device name that is returned in {@link #BROADCAST_CONNECTION_STATE} with state {@link #STATE_CONNECTED}.
*/
public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME";
public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE";
public static final String EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI";
public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE";
public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE";
public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY";
public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY";
@Deprecated
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE";
public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE";
public static final int STATE_LINK_LOSS = -1;
public static final int STATE_DISCONNECTED = 0;
public static final int STATE_CONNECTED = 1;
public static final int STATE_CONNECTING = 2;
public static final int STATE_DISCONNECTING = 3;
private LoggableBleManager<BleManagerCallbacks> bleManager;
private Handler handler;
protected boolean bound;
private boolean activityIsChangingConfiguration;
private BluetoothDevice bluetoothDevice;
private String deviceName;
private ILogSession logSession;
private final BroadcastReceiver bluetoothStateBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
final ILogger logger = getBinder();
final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(state);
logger.log(Log.DEBUG, stateString);
switch (state) {
case BluetoothAdapter.STATE_ON:
onBluetoothEnabled();
break;
case BluetoothAdapter.STATE_TURNING_OFF:
case BluetoothAdapter.STATE_OFF:
onBluetoothDisabled();
break;
}
}
private String state2String(final int state) {
switch (state) {
case BluetoothAdapter.STATE_TURNING_ON:
return "TURNING ON";
case BluetoothAdapter.STATE_ON:
return "ON";
case BluetoothAdapter.STATE_TURNING_OFF:
return "TURNING OFF";
case BluetoothAdapter.STATE_OFF:
return "OFF";
default:
return "UNKNOWN (" + state + ")";
}
}
};
public class LocalBinder extends Binder implements ILogger {
/**
* Disconnects from the sensor.
*/
public final void disconnect() {
final int state = bleManager.getConnectionState();
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 <code>false</code>, we will turn off battery level notifications in onUnbind(..) method below.
*
* @param changing true if the bound activity is finishing
*/
public void setActivityIsChangingConfiguration(final boolean changing) {
activityIsChangingConfiguration = changing;
}
/**
* Returns the device address
*
* @return device address
*/
public String getDeviceAddress() {
return bluetoothDevice.getAddress();
}
/**
* Returns the device name
*
* @return the device name
*/
public String getDeviceName() {
return deviceName;
}
/**
* Returns the Bluetooth device
*
* @return the Bluetooth device
*/
public BluetoothDevice getBluetoothDevice() {
return bluetoothDevice;
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
*
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
public boolean isConnected() {
return bleManager.isConnected();
}
/**
* Returns the connection state of given device.
*
* @return the connection state, as in {@link BleManager#getConnectionState()}.
*/
public int getConnectionState() {
return bleManager.getConnectionState();
}
/**
* 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 <code>null</code> if the nRF Logger app was not installed.
*
* @return the log session
*/
public ILogSession getLogSession() {
return logSession;
}
@Override
public void log(final int level, @NonNull final String message) {
Logger.log(logSession, level, message);
}
@Override
public void log(final int level, final @StringRes int messageRes, final Object... params) {
Logger.log(logSession, level, messageRes, params);
}
}
/**
* 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 Handler getHandler() {
return handler;
}
/**
* 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 LocalBinder getBinder() {
// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
return new LocalBinder();
}
@Override
public IBinder onBind(final Intent intent) {
bound = true;
return getBinder();
}
@Override
public final void onRebind(final 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 {@link LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true.
*/
protected void onRebind() {
// empty default implementation
}
@Override
public final boolean onUnbind(final Intent intent) {
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 void onUnbind() {
// empty default implementation
}
@SuppressWarnings("unchecked")
@Override
public void onCreate() {
super.onCreate();
handler = new Handler();
// Initialize the manager
bleManager = initializeManager();
bleManager.setGattCallbacks(this);
// Register broadcast receivers
registerReceiver(bluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
// Service has now been created
onServiceCreated();
// Call onBluetoothEnabled if Bluetooth enabled
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter.isEnabled()) {
onBluetoothEnabled();
}
}
/**
* Called when the service has been created, before the {@link #onBluetoothEnabled()} is called.
*/
protected void onServiceCreated() {
// empty default implementation
}
/**
* Initializes the Ble Manager responsible for connecting to a single device.
*
* @return a new BleManager object
*/
@SuppressWarnings("rawtypes")
protected abstract LoggableBleManager initializeManager();
/**
* This method returns whether autoConnect option should be used.
*
* @return true to use autoConnect feature, false (default) otherwise.
*/
protected boolean shouldAutoConnect() {
return false;
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS))
throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key");
final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI);
logSession = Logger.openSession(getApplicationContext(), logUri);
deviceName = intent.getStringExtra(EXTRA_DEVICE_NAME);
Logger.i(logSession, "Service started");
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
bluetoothDevice = adapter.getRemoteDevice(deviceAddress);
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 void onServiceStarted() {
// empty default implementation
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
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
public void onDestroy() {
super.onDestroy();
// Unregister broadcast receivers
unregisterReceiver(bluetoothStateBroadcastReceiver);
// shutdown the manager
bleManager.close();
Logger.i(logSession, "Service destroyed");
bleManager = null;
bluetoothDevice = null;
deviceName = null;
logSession = null;
handler = null;
}
/**
* Method called when Bluetooth Adapter has been disabled.
*/
protected void 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 void onBluetoothEnabled() {
// empty default implementation
}
@Override
public void onDeviceConnecting(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceConnected(@NonNull final BluetoothDevice device) {
final Intent broadcast = new 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
public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) {
// Notify user about changing the state to DISCONNECTING
final Intent broadcast = new 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 {@link #stopService()} method must be called when done.
*
* @return true (default) to automatically stop the service when device is disconnected. False otherwise.
*/
protected boolean stopWhenDisconnected() {
return true;
}
@Override
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
// 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.
final Intent broadcast = new 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 void stopService() {
// user requested disconnection. We must stop the service
Logger.v(logSession, "Stopping service...");
stopSelf();
}
@Override
public void onLinkLossOccurred(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) {
final Intent broadcast = new 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
public void onDeviceReady(@NonNull final BluetoothDevice device) {
final Intent broadcast = new Intent(BROADCAST_DEVICE_READY);
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onDeviceNotSupported(@NonNull final BluetoothDevice device) {
final Intent broadcast = new 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
public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) {
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
broadcast.putExtra(EXTRA_BATTERY_LEVEL, value);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBondingRequired(@NonNull final BluetoothDevice device) {
showToast(R.string.csc_bonding);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBonded(@NonNull final BluetoothDevice device) {
showToast(R.string.csc_bonded);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onBondingFailed(@NonNull final BluetoothDevice device) {
showToast(R.string.csc_bonding_failed);
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
@Override
public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) {
final Intent broadcast = new 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 void showToast(final int messageResId) {
handler.post(() -> Toast.makeText(BleProfileService.this, 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 void showToast(final String message) {
handler.post(() -> Toast.makeText(BleProfileService.this, message, Toast.LENGTH_SHORT).show());
}
/**
* Returns the log session that can be used to append log entries. The method returns <code>null</code> if the nRF Logger app was not installed. It is safe to use logger when
* {@link #onServiceStarted()} has been called.
*
* @return the log session
*/
protected ILogSession getLogSession() {
return logSession;
}
/**
* Returns the device address
*
* @return device address
*/
protected String getDeviceAddress() {
return bluetoothDevice.getAddress();
}
/**
* Returns the Bluetooth device object
*
* @return bluetooth device
*/
protected BluetoothDevice getBluetoothDevice() {
return bluetoothDevice;
}
/**
* Returns the device name
*
* @return the device name
*/
protected String getDeviceName() {
return deviceName;
}
/**
* Returns <code>true</code> if the device is connected to the sensor.
*
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
*/
protected boolean isConnected() {
return bleManager != null && bleManager.isConnected();
}
}

View File

@@ -1,21 +1,15 @@
package no.nordicsemi.android.broadcast
package no.nordicsemi.android.csc.service
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import no.nordicsemi.android.events.BluetoothReadDataEvent
import no.nordicsemi.android.csc.events.CSCServiceEvent
import no.nordicsemi.android.service.BluetoothDataReadBroadcast
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BluetoothDataReadBroadcast @Inject constructor() {
private val _event = MutableSharedFlow<BluetoothReadDataEvent>(
replay = 1,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<BluetoothReadDataEvent> = _event
class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast<CSCServiceEvent>() {
private val _wheelSize = MutableSharedFlow<Int>(
replay = 1,
@@ -24,10 +18,6 @@ class BluetoothDataReadBroadcast @Inject constructor() {
)
val wheelSize: SharedFlow<Int> = _wheelSize
fun offer(newEvent: BluetoothReadDataEvent) {
_event.tryEmit(newEvent)
}
fun setWheelSize(size: Int) {
_wheelSize.tryEmit(size)
}

View File

@@ -29,17 +29,16 @@ import android.util.Log
import androidx.annotation.FloatRange
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.csc.batery.BatteryManager
import no.nordicsemi.android.csc.batery.CSCMeasurementParser.parse
import no.nordicsemi.android.csc.service.CSCMeasurementParser.parse
import no.nordicsemi.android.csc.view.CSCSettings
import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.service.BatteryManager
import java.util.*
private const val SETTINGS_WHEEL_SIZE_DEFAULT = 2340
internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks?>(context) {
internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks>(context) {
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var wheelSize = SETTINGS_WHEEL_SIZE_DEFAULT
private var wheelSize = CSCSettings.DefaultWheelSize.VALUE
override fun getGattCallback(): BatteryManagerGattCallback {
return CSCManagerGattCallback()
@@ -77,7 +76,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
@FloatRange(from = 0.0) distance: Float,
@FloatRange(from = 0.0) speed: Float
) {
mCallbacks!!.onDistanceChanged(device, totalDistance, distance, speed)
mCallbacks?.onDistanceChanged(device, totalDistance, distance, speed)
}
override fun onCrankDataChanged(
@@ -85,7 +84,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
@FloatRange(from = 0.0) crankCadence: Float,
gearRatio: Float
) {
mCallbacks!!.onCrankDataChanged(device, crankCadence, gearRatio)
mCallbacks?.onCrankDataChanged(device, crankCadence, gearRatio)
}
override fun onInvalidDataReceived(

View File

@@ -22,5 +22,6 @@
package no.nordicsemi.android.csc.service
import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback
import no.nordicsemi.android.service.BatteryManagerCallbacks
internal interface CSCManagerCallbacks : BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback

View File

@@ -19,7 +19,7 @@
* 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.csc.batery
package no.nordicsemi.android.csc.service
import no.nordicsemi.android.ble.data.Data

View File

@@ -1,89 +1,39 @@
/*
* 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.csc.service
import android.app.Notification
import android.app.NotificationManager
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.broadcast.BluetoothDataReadBroadcast
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.batery.LoggableBleManager
import no.nordicsemi.android.events.CrankDataChanged
import no.nordicsemi.android.events.OnBatteryLevelChanged
import no.nordicsemi.android.events.OnDistanceChangedEvent
import no.nordicsemi.android.log.Logger
import no.nordicsemi.android.csc.events.CrankDataChanged
import no.nordicsemi.android.csc.events.OnBatteryLevelChanged
import no.nordicsemi.android.csc.events.OnDistanceChangedEvent
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.service.LoggableBleManager
import javax.inject.Inject
@AndroidEntryPoint
internal class CSCService : BleProfileService(), CSCManagerCallbacks {
private var manager: CSCManager? = null
internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallbacks {
@Inject lateinit var localBroadcast: BluetoothDataReadBroadcast
@Inject
lateinit var localBroadcast: CSCDataReadBroadcast
override fun initializeManager(): LoggableBleManager<CSCManagerCallbacks?> {
return CSCManager(this).also { manager = it }
override val manager: CSCManager by lazy {
CSCManager(this).apply {
setGattCallbacks(this@CSCService)
}
}
override fun initializeManager(): LoggableBleManager<CSCManagerCallbacks> {
return manager
}
override fun onCreate() {
super.onCreate()
val filter = IntentFilter()
filter.addAction(ACTION_DISCONNECT)
registerReceiver(disconnectActionBroadcastReceiver, filter)
localBroadcast.wheelSize.onEach {
manager?.setWheelSize(it)
}.launchIn(GlobalScope)
}
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()
unregisterReceiver(disconnectActionBroadcastReceiver)
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()
manager.setWheelSize(it)
}.launchIn(lifecycleScope)
}
override fun onDistanceChanged(
@@ -106,84 +56,4 @@ internal class CSCService : BleProfileService(), CSCManagerCallbacks {
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
localBroadcast.offer(OnBatteryLevelChanged(bluetoothDevice, batteryLevel))
}
/**
* 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)
}
/**
* This broadcast receiver listens for [.ACTION_DISCONNECT] that may be fired by pressing Disconnect action button on the notification.
*/
private val disconnectActionBroadcastReceiver: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Logger.i(logSession, "[Notification] Disconnect action pressed")
if (isConnected) binder.disconnect() else stopSelf()
}
}
companion object {
private const val ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.csc.ACTION_DISCONNECT"
private const val NOTIFICATION_ID = 200
}
}
}

View File

@@ -0,0 +1,9 @@
package no.nordicsemi.android.csc.view
object CSCSettings {
object DefaultWheelSize {
const val NAME = "60-622"
const val VALUE = 2340
}
}

View File

@@ -1,7 +1,5 @@
package no.nordicsemi.android.csc.view
import no.nordicsemi.android.events.EMPTY
internal sealed class CSCViewState {
fun ensureConnectedState(): CSCViewConnectedState {
@@ -30,7 +28,7 @@ internal data class CSCViewConnectedState(
val totalDistance: Float = 0f,
val gearRatio: Float = 0f,
val batteryLevel: Int = 0,
val wheelSize: String = String.EMPTY
val wheelSize: String = CSCSettings.DefaultWheelSize.NAME
) : CSCViewState() {
fun displaySpeed(): String {

View File

@@ -25,7 +25,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.service.CSCService
import no.nordicsemi.android.events.exhaustive
import no.nordicsemi.android.csc.viewmodel.CscViewModel
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.android.utils.isServiceRunning
@Composable
internal fun CscScreen(navController: NavController, viewModel: CscViewModel = hiltViewModel()) {
@@ -37,11 +39,6 @@ internal fun CscScreen(navController: NavController, viewModel: CscViewModel = h
secondScreenResult?.value?.let {
viewModel.onEvent(OnBluetoothDeviceSelected(it))
val intent = Intent(LocalContext.current, CSCService::class.java).apply {
putExtra("no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS", it.address)
}
LocalContext.current.startService(intent)
navController.currentBackStackEntry
?.savedStateHandle
?.set("result", null)
@@ -77,7 +74,14 @@ private fun NotConnectedScreen(
onEvent(OnMovedToScannerScreen)
}
if (LocalContext.current.isServiceRunning(CSCService::class.java.name)) {
val intent = Intent(LocalContext.current, CSCService::class.java)
LocalContext.current.stopService(intent)
}
NotConnectedView(onEvent)
LocalContext.current.stopService(Intent(LocalContext.current, CSCService::class.java))
}
@Composable
@@ -105,6 +109,11 @@ private fun ConnectedView(state: CSCViewConnectedState, onEvent: (CSCViewEvent)
SelectWheelSizeDialog { onEvent(it) }
}
if (!LocalContext.current.isServiceRunning(CSCService::class.java.name)) {
val intent = Intent(LocalContext.current, CSCService::class.java)
LocalContext.current.startService(intent)
}
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -118,7 +127,6 @@ private fun ConnectedView(state: CSCViewConnectedState, onEvent: (CSCViewEvent)
Button(onClick = { onEvent(OnDisconnectButtonClick) }) {
Text(text = stringResource(id = R.string.csc_disconnect))
}
}
}

View File

@@ -1,4 +1,4 @@
package no.nordicsemi.android.csc.view
package no.nordicsemi.android.csc.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -8,27 +8,44 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import no.nordicsemi.android.broadcast.BluetoothDataReadBroadcast
import no.nordicsemi.android.events.CSCServiceEvent
import no.nordicsemi.android.events.CrankDataChanged
import no.nordicsemi.android.events.OnBatteryLevelChanged
import no.nordicsemi.android.events.OnDistanceChangedEvent
import no.nordicsemi.android.events.exhaustive
import no.nordicsemi.android.csc.events.CSCServiceEvent
import no.nordicsemi.android.csc.events.CrankDataChanged
import no.nordicsemi.android.csc.events.OnBatteryLevelChanged
import no.nordicsemi.android.csc.events.OnDistanceChangedEvent
import no.nordicsemi.android.csc.service.CSCDataReadBroadcast
import no.nordicsemi.android.csc.view.CSCViewConnectedState
import no.nordicsemi.android.csc.view.CSCViewEvent
import no.nordicsemi.android.csc.view.CSCViewNotConnectedState
import no.nordicsemi.android.csc.view.CSCViewState
import no.nordicsemi.android.csc.view.OnBluetoothDeviceSelected
import no.nordicsemi.android.csc.view.OnConnectButtonClick
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
import no.nordicsemi.android.csc.view.OnMovedToScannerScreen
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
import no.nordicsemi.android.csc.view.OnWheelSizeSelected
import no.nordicsemi.android.scanner.tools.SelectedBluetoothDeviceHolder
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
internal class CscViewModel @Inject constructor(
private val localBroadcast: BluetoothDataReadBroadcast
private val localBroadcast: CSCDataReadBroadcast,
private val deviceHolder: SelectedBluetoothDeviceHolder
) : ViewModel() {
val state = MutableStateFlow<CSCViewState>(CSCViewNotConnectedState())
val state = MutableStateFlow(createInitialState())
init {
localBroadcast.events.onEach {
(it as? CSCServiceEvent)?.let { withContext(Dispatchers.Main) { consumeEvent(it) }}
withContext(Dispatchers.Main) { consumeEvent(it) }
}.launchIn(viewModelScope)
}
private fun createInitialState(): CSCViewState {
return deviceHolder.device?.let { CSCViewConnectedState() } ?: CSCViewNotConnectedState()
}
private fun consumeEvent(event: CSCServiceEvent) {
val newValue = when (event) {
is CrankDataChanged -> createNewState(event)
@@ -64,7 +81,7 @@ internal class CscViewModel @Inject constructor(
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
is OnWheelSizeSelected -> onWheelSizeChanged(event)
OnDisconnectButtonClick -> TODO()
OnDisconnectButtonClick -> onDisconnectButtonClick()
OnConnectButtonClick -> onConnectButtonClick()
OnMovedToScannerScreen -> onOnMovedToScannerScreen()
is OnBluetoothDeviceSelected -> onBluetoothDeviceSelected()
@@ -87,6 +104,10 @@ internal class CscViewModel @Inject constructor(
))
}
private fun onDisconnectButtonClick() {
state.tryEmit(CSCViewNotConnectedState())
}
private fun onConnectButtonClick() {
state.tryEmit(state.value.ensureDisconnectedState().copy(showScannerDialog = true))
}

View File

@@ -2,16 +2,10 @@
<resources>
<string name="csc_title">Cyclic and speed cadence</string>
<string name="csc_bonding">Bonding with the device&#8230;</string>
<string name="csc_bonded">The device is now bonded.</string>
<string name="csc_bonding_failed">Bonding failed.</string>
<string name="csc_disconnect">Disconnect</string>
<string name="csc_no_connection">No device connected</string>
<string name="csc_connect">Connect</string>
<string name="csc_notification_connected_message">%s is connected.</string>
<string name="scs_field_speed">Speed</string>
<string name="scs_field_cadence">Cadence</string>
<string name="scs_field_distance">Distance</string>

View File

@@ -2,8 +2,8 @@ apply from: rootProject.file("library.gradle")
apply plugin: 'kotlin-parcelize'
dependencies {
implementation project(":lib_utils")
implementation project(":lib_theme")
implementation project(":lib_events")
implementation libs.material
implementation libs.google.permissions

View File

@@ -0,0 +1,30 @@
package no.nordicsemi.android.scanner
import android.bluetooth.BluetoothAdapter
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import no.nordicsemi.android.scanner.tools.SelectedBluetoothDeviceHolder
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object HiltModule {
@Provides
fun createNordicBleScanner(): BluetoothAdapter? {
return BluetoothAdapter.getDefaultAdapter()
}
@Singleton
@Provides
fun createSelectedBluetoothDeviceHolder(
@ApplicationContext context: Context,
bluetoothAdapter: BluetoothAdapter?
): SelectedBluetoothDeviceHolder {
return SelectedBluetoothDeviceHolder(context, bluetoothAdapter)
}
}

View File

@@ -8,20 +8,17 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import no.nordicsemi.android.events.exhaustive
import no.nordicsemi.android.scanner.tools.ScannerStatus
import no.nordicsemi.android.scanner.ui.BluetoothNotAvailableScreen
import no.nordicsemi.android.scanner.ui.BluetoothNotEnabledScreen
import no.nordicsemi.android.scanner.ui.NordicBleScannerViewModel
import no.nordicsemi.android.scanner.ui.RequestPermissionScreen
import no.nordicsemi.android.scanner.ui.ScanDeviceScreen
import no.nordicsemi.android.scanner.ui.ScannerViewEvent
import no.nordicsemi.android.scanner.view.*
import no.nordicsemi.android.scanner.viewmodel.NordicBleScannerViewModel
import no.nordicsemi.android.scanner.viewmodel.ScannerViewEvent
import no.nordicsemi.android.utils.exhaustive
@Composable
fun ScannerRoute(navController: NavController) {
val viewModel = hiltViewModel<NordicBleScannerViewModel>()
val scannerStatus = viewModel.state.collectAsState().value.scannerStatus
val scannerStatus = viewModel.state.collectAsState().value
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__devices_list)) })

View File

@@ -3,8 +3,6 @@ package no.nordicsemi.android.scanner.tools
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@@ -13,22 +11,6 @@ internal class NordicBleScanner @Inject constructor(private val bleAdapter: Blue
val scannerResult = MutableStateFlow<ScanningResult>(DeviceListResult())
private var isScanning = false
private val scanner by lazy { bleAdapter?.bluetoothLeScanner }
private val devices = mutableListOf<BluetoothDevice>()
private val scanningCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.device?.let { devices.addIfNotExist(it) }
scannerResult.value = DeviceListResult(devices)
}
override fun onScanFailed(errorCode: Int) {
scannerResult.value = ScanningErrorResult
}
}
fun getBluetoothStatus(): ScannerStatus {
return when {
bleAdapter == null -> ScannerStatus.NOT_AVAILABLE
@@ -36,22 +18,6 @@ internal class NordicBleScanner @Inject constructor(private val bleAdapter: Blue
else -> ScannerStatus.DISABLED
}
}
fun startScanning() {
if (isScanning) {
return
}
isScanning = true
scanner?.startScan(scanningCallback)
}
fun stopScanning() {
if (!isScanning) {
return
}
isScanning = false
scanner?.stopScan(scanningCallback)
}
}
sealed class ScanningResult

View File

@@ -1,5 +1,5 @@
package no.nordicsemi.android.scanner.tools
enum class ScannerStatus {
internal enum class ScannerStatus {
PERMISSION_REQUIRED, ENABLED, DISABLED, NOT_AVAILABLE
}

View File

@@ -0,0 +1,25 @@
package no.nordicsemi.android.scanner.tools
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.companion.CompanionDeviceManager
import android.content.Context
class SelectedBluetoothDeviceHolder constructor(
private val context: Context,
private val bluetoothAdapter: BluetoothAdapter?
) {
val device: BluetoothDevice?
get() {
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
}
fun forgetDevice() {
device?.let {
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
deviceManager.disassociate(it.address)
}
}
}

View File

@@ -1,4 +1,4 @@
package no.nordicsemi.android.scanner.ui
package no.nordicsemi.android.scanner.view
import android.app.Activity
import android.bluetooth.BluetoothAdapter

View File

@@ -1,17 +1,10 @@
package no.nordicsemi.android.scanner.ui
package no.nordicsemi.android.scanner.view
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

View File

@@ -1,9 +1,10 @@
package no.nordicsemi.android.scanner.ui
package no.nordicsemi.android.scanner.view
import android.app.Activity
import android.bluetooth.BluetoothDevice
import android.bluetooth.le.ScanResult
import android.companion.AssociationRequest
import android.companion.BluetoothDeviceFilter
import android.companion.BluetoothLeDeviceFilter
import android.companion.CompanionDeviceManager
import android.content.Context
import android.content.IntentSender
@@ -20,10 +21,11 @@ import androidx.navigation.NavController
@Composable
fun ScanDeviceScreen(navController: NavController,) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
val deviceFilter = BluetoothLeDeviceFilter.Builder()
.build()
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.build()
val deviceManager =
@@ -32,13 +34,18 @@ fun ScanDeviceScreen(navController: NavController,) {
val contract = ActivityResultContracts.StartIntentSenderForResult()
val launcher = rememberLauncherForActivityResult(contract = contract, onResult = {
if (it.resultCode == Activity.RESULT_OK) {
val deviceToPair: BluetoothDevice? = it.data?.getParcelableExtra(
CompanionDeviceManager.EXTRA_DEVICE)
//Sometimes result is ScanResult & sometimes BluetoothDevice
val device: BluetoothDevice = try {
it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)!!
} catch (e: Exception) {
(it.data?.getParcelableExtra<ScanResult>(CompanionDeviceManager.EXTRA_DEVICE))!!.device
}
navController.previousBackStackEntry
?.savedStateHandle
?.set("result", deviceToPair)
navController.popBackStack()
?.set("result", device)
}
navController.popBackStack()
})
val hasBeenInvoked = remember { mutableStateOf(false) }

View File

@@ -1,11 +1,11 @@
package no.nordicsemi.android.scanner.ui
package no.nordicsemi.android.scanner.viewmodel
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import no.nordicsemi.android.events.exhaustive
import no.nordicsemi.android.scanner.tools.NordicBleScanner
import no.nordicsemi.android.scanner.tools.ScannerStatus
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
@@ -14,9 +14,7 @@ internal class NordicBleScannerViewModel @Inject constructor(
) : ViewModel() {
val state =
MutableStateFlow(NordicBleScannerState(scannerStatus = ScannerStatus.PERMISSION_REQUIRED))
val scannerResult = bleScanner.scannerResult
MutableStateFlow(ScannerStatus.PERMISSION_REQUIRED)
fun onEvent(event: ScannerViewEvent) {
when (event) {
@@ -26,19 +24,14 @@ internal class NordicBleScannerViewModel @Inject constructor(
}
private fun onPermissionChecked() {
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
state.value = bleScanner.getBluetoothStatus()
}
private fun onBluetoothEnabled() {
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
bleScanner.startScanning()
state.value = bleScanner.getBluetoothStatus()
}
}
enum class ScannerViewEvent {
internal enum class ScannerViewEvent {
PERMISSION_CHECKED, BLUETOOTH_ENABLED
}
internal data class NordicBleScannerState(
val scannerStatus: ScannerStatus
)

View File

@@ -1,7 +0,0 @@
apply from: rootProject.file("library.gradle")
dependencies {
implementation project(":lib_events")
implementation libs.kotlin.coroutines
}

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="no.nordicsemi.android.broadcast">
</manifest>

View File

@@ -1,3 +0,0 @@
apply from: rootProject.file("library.gradle")
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'

View File

@@ -1,3 +0,0 @@
package no.nordicsemi.android.events
sealed class BluetoothReadDataEvent

View File

@@ -1,7 +0,0 @@
package no.nordicsemi.android.events
val <T> T.exhaustive
get() = this
val String.Companion.EMPTY
get() = ""

View File

@@ -1,17 +0,0 @@
package no.nordicsemi.android.scanner
import android.bluetooth.BluetoothAdapter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object HiltModule {
@Provides
fun createNordicBleScanner(): BluetoothAdapter? {
return BluetoothAdapter.getDefaultAdapter()
}
}

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

@@ -1,4 +1,4 @@
package no.nordicsemi.android.broadcast
package no.nordicsemi.android.service
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("no.nordicsemi.android.broadcast.test", appContext.packageName)
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

@@ -1,4 +1,4 @@
package no.nordicsemi.android.csc.batery
package no.nordicsemi.android.service
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
@@ -9,7 +9,6 @@ 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.csc.service.BatteryManagerCallbacks
import no.nordicsemi.android.log.LogContract
import java.util.*
@@ -38,7 +37,7 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
) {
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
this@BatteryManager.batteryLevel = batteryLevel
mCallbacks!!.onBatteryLevelChanged(device, batteryLevel)
mCallbacks?.onBatteryLevelChanged(device, batteryLevel)
}
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {

View File

@@ -1,4 +1,4 @@
package no.nordicsemi.android.csc.service
package no.nordicsemi.android.service
import no.nordicsemi.android.ble.BleManagerCallbacks
import no.nordicsemi.android.ble.common.profile.battery.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

@@ -1,4 +1,4 @@
package no.nordicsemi.android.csc.batery
package no.nordicsemi.android.service
import android.content.Context
import android.util.Log
@@ -13,17 +13,7 @@ import no.nordicsemi.android.log.Logger
*
* @param <T> the callbacks class.
</T> */
abstract class LoggableBleManager<T : BleManagerCallbacks?>
/**
* The manager constructor.
*
*
* After constructing the manager, the callbacks object must be set with
* [.setGattCallbacks].
*
* @param context the context.
*/
(context: Context) : LegacyBleManager<T>(context) {
abstract class LoggableBleManager<T : BleManagerCallbacks?>(context: Context) : LegacyBleManager<T>(context) {
private var logSession: ILogSession? = null
/**

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

@@ -1,4 +1,4 @@
package no.nordicsemi.android.broadcast
package no.nordicsemi.android.service
import org.junit.Test

2
lib_utils/build.gradle Normal file
View File

@@ -0,0 +1,2 @@
apply from: rootProject.file("library.gradle")
apply plugin: 'kotlin-parcelize'

View File

@@ -1,4 +1,4 @@
package no.nordicsemi.android.events
package no.nordicsemi.android.utils
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("no.nordicsemi.android.events.test", appContext.packageName)
assertEquals("no.nordicsemi.android.utils.test", appContext.packageName)
}
}

View File

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

View File

@@ -0,0 +1,16 @@
package no.nordicsemi.android.utils
import android.app.ActivityManager
import android.content.Context
val <T> T.exhaustive
get() = this
val String.Companion.EMPTY
get() = ""
fun Context.isServiceRunning(serviceClassName: String): Boolean {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val services = activityManager.getRunningServices(Integer.MAX_VALUE)
return services.find { it.service.className == serviceClassName } != null
}

View File

@@ -1,4 +1,4 @@
package no.nordicsemi.android.events
package no.nordicsemi.android.utils
import org.junit.Test

View File

@@ -14,6 +14,7 @@ dependencyResolutionManagement {
alias('nordic-log').to('no.nordicsemi.android:log:2.3.0')
alias('nordic-scanner').to('no.nordicsemi.android.support.v18:scanner:1.5.0')
alias('localbroadcastmanager').to('androidx.localbroadcastmanager:localbroadcastmanager:1.0.0')
alias('material').to('com.google.android.material:material:1.4.0')
version('lifecycle', '2.3.1')
@@ -57,12 +58,14 @@ dependencyResolutionManagement {
rootProject.name = "Android-nRF-Toolbox"
include ':app'
include ':feature_csc'
include ':lib_broadcast'
include ':lib_events'
include ':feature_scanner'
include ':lib_service'
include ':lib_theme'
include ':lib_utils'
if (file('../Android-BLE-Library').exists()) {
includeBuild('../Android-BLE-Library')
}
include ':lib_theme'
include ':lib_scanner'