mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 15:34:26 +01:00
Modernization of modular approach
This commit is contained in:
@@ -51,10 +51,8 @@ dependencies {
|
|||||||
//Hilt requires to implement every module in the main app module
|
//Hilt requires to implement every module in the main app module
|
||||||
//https://github.com/google/dagger/issues/2123
|
//https://github.com/google/dagger/issues/2123
|
||||||
implementation project(":feature_csc")
|
implementation project(":feature_csc")
|
||||||
implementation project(":lib_broadcast")
|
|
||||||
implementation project(":lib_events")
|
|
||||||
implementation project(":lib_theme")
|
implementation project(":lib_theme")
|
||||||
implementation project(":lib_scanner")
|
implementation project(':feature_scanner')
|
||||||
|
|
||||||
implementation libs.nordic.ble.common
|
implementation libs.nordic.ble.common
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
compose_version = '1.1.0-alpha03'
|
compose_version = '1.1.0-alpha03'
|
||||||
kotlin_version = '1.5.30'
|
kotlin_version = '1.5.30'
|
||||||
android_api_version = 31
|
android_api_version = 31
|
||||||
android_min_api_version = 21
|
android_min_api_version = 26
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ apply from: rootProject.file("library.gradle")
|
|||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":lib_broadcast")
|
implementation project(":lib_service")
|
||||||
implementation project(":lib_events")
|
|
||||||
implementation project(":lib_theme")
|
implementation project(":lib_theme")
|
||||||
implementation project(":lib_scanner")
|
implementation project(':feature_scanner')
|
||||||
|
implementation project(":lib_utils")
|
||||||
|
|
||||||
implementation libs.nordic.ble.common
|
implementation libs.nordic.ble.common
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package no.nordicsemi.android.events
|
package no.nordicsemi.android.csc.events
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
sealed class CSCServiceEvent : BluetoothReadDataEvent(), Parcelable
|
sealed class CSCServiceEvent : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class OnDistanceChangedEvent(
|
data class OnDistanceChangedEvent(
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
package no.nordicsemi.android.broadcast
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
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.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BluetoothDataReadBroadcast @Inject constructor() {
|
class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast<CSCServiceEvent>() {
|
||||||
|
|
||||||
private val _event = MutableSharedFlow<BluetoothReadDataEvent>(
|
|
||||||
replay = 1,
|
|
||||||
extraBufferCapacity = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
val events: SharedFlow<BluetoothReadDataEvent> = _event
|
|
||||||
|
|
||||||
private val _wheelSize = MutableSharedFlow<Int>(
|
private val _wheelSize = MutableSharedFlow<Int>(
|
||||||
replay = 1,
|
replay = 1,
|
||||||
@@ -24,10 +18,6 @@ class BluetoothDataReadBroadcast @Inject constructor() {
|
|||||||
)
|
)
|
||||||
val wheelSize: SharedFlow<Int> = _wheelSize
|
val wheelSize: SharedFlow<Int> = _wheelSize
|
||||||
|
|
||||||
fun offer(newEvent: BluetoothReadDataEvent) {
|
|
||||||
_event.tryEmit(newEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWheelSize(size: Int) {
|
fun setWheelSize(size: Int) {
|
||||||
_wheelSize.tryEmit(size)
|
_wheelSize.tryEmit(size)
|
||||||
}
|
}
|
||||||
@@ -29,17 +29,16 @@ import android.util.Log
|
|||||||
import androidx.annotation.FloatRange
|
import androidx.annotation.FloatRange
|
||||||
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
|
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
import no.nordicsemi.android.csc.batery.BatteryManager
|
import no.nordicsemi.android.csc.service.CSCMeasurementParser.parse
|
||||||
import no.nordicsemi.android.csc.batery.CSCMeasurementParser.parse
|
import no.nordicsemi.android.csc.view.CSCSettings
|
||||||
import no.nordicsemi.android.log.LogContract
|
import no.nordicsemi.android.log.LogContract
|
||||||
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
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 cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var wheelSize = SETTINGS_WHEEL_SIZE_DEFAULT
|
private var wheelSize = CSCSettings.DefaultWheelSize.VALUE
|
||||||
|
|
||||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||||
return CSCManagerGattCallback()
|
return CSCManagerGattCallback()
|
||||||
@@ -77,7 +76,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
|
|||||||
@FloatRange(from = 0.0) distance: Float,
|
@FloatRange(from = 0.0) distance: Float,
|
||||||
@FloatRange(from = 0.0) speed: Float
|
@FloatRange(from = 0.0) speed: Float
|
||||||
) {
|
) {
|
||||||
mCallbacks!!.onDistanceChanged(device, totalDistance, distance, speed)
|
mCallbacks?.onDistanceChanged(device, totalDistance, distance, speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCrankDataChanged(
|
override fun onCrankDataChanged(
|
||||||
@@ -85,7 +84,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
|
|||||||
@FloatRange(from = 0.0) crankCadence: Float,
|
@FloatRange(from = 0.0) crankCadence: Float,
|
||||||
gearRatio: Float
|
gearRatio: Float
|
||||||
) {
|
) {
|
||||||
mCallbacks!!.onCrankDataChanged(device, crankCadence, gearRatio)
|
mCallbacks?.onCrankDataChanged(device, crankCadence, gearRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInvalidDataReceived(
|
override fun onInvalidDataReceived(
|
||||||
|
|||||||
@@ -22,5 +22,6 @@
|
|||||||
package no.nordicsemi.android.csc.service
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback
|
import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback
|
||||||
|
import no.nordicsemi.android.service.BatteryManagerCallbacks
|
||||||
|
|
||||||
internal interface CSCManagerCallbacks : BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback
|
internal interface CSCManagerCallbacks : BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback
|
||||||
|
|||||||
@@ -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
|
* 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.
|
* 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
|
import no.nordicsemi.android.ble.data.Data
|
||||||
|
|
||||||
@@ -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
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.content.BroadcastReceiver
|
import androidx.lifecycle.lifecycleScope
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import no.nordicsemi.android.broadcast.BluetoothDataReadBroadcast
|
import no.nordicsemi.android.csc.events.CrankDataChanged
|
||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.events.OnBatteryLevelChanged
|
||||||
import no.nordicsemi.android.csc.batery.LoggableBleManager
|
import no.nordicsemi.android.csc.events.OnDistanceChangedEvent
|
||||||
import no.nordicsemi.android.events.CrankDataChanged
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
import no.nordicsemi.android.events.OnBatteryLevelChanged
|
import no.nordicsemi.android.service.LoggableBleManager
|
||||||
import no.nordicsemi.android.events.OnDistanceChangedEvent
|
|
||||||
import no.nordicsemi.android.log.Logger
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class CSCService : BleProfileService(), CSCManagerCallbacks {
|
internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallbacks {
|
||||||
private var manager: CSCManager? = null
|
|
||||||
|
|
||||||
@Inject lateinit var localBroadcast: BluetoothDataReadBroadcast
|
@Inject
|
||||||
|
lateinit var localBroadcast: CSCDataReadBroadcast
|
||||||
|
|
||||||
override fun initializeManager(): LoggableBleManager<CSCManagerCallbacks?> {
|
override val manager: CSCManager by lazy {
|
||||||
return CSCManager(this).also { manager = it }
|
CSCManager(this).apply {
|
||||||
|
setGattCallbacks(this@CSCService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initializeManager(): LoggableBleManager<CSCManagerCallbacks> {
|
||||||
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
val filter = IntentFilter()
|
|
||||||
filter.addAction(ACTION_DISCONNECT)
|
|
||||||
registerReceiver(disconnectActionBroadcastReceiver, filter)
|
|
||||||
|
|
||||||
localBroadcast.wheelSize.onEach {
|
localBroadcast.wheelSize.onEach {
|
||||||
manager?.setWheelSize(it)
|
manager.setWheelSize(it)
|
||||||
}.launchIn(GlobalScope)
|
}.launchIn(lifecycleScope)
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDistanceChanged(
|
override fun onDistanceChanged(
|
||||||
@@ -106,84 +56,4 @@ internal class CSCService : BleProfileService(), CSCManagerCallbacks {
|
|||||||
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
|
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
|
||||||
localBroadcast.offer(OnBatteryLevelChanged(bluetoothDevice, batteryLevel))
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
object CSCSettings {
|
||||||
|
|
||||||
|
object DefaultWheelSize {
|
||||||
|
const val NAME = "60-622"
|
||||||
|
const val VALUE = 2340
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package no.nordicsemi.android.csc.view
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
import no.nordicsemi.android.events.EMPTY
|
|
||||||
|
|
||||||
internal sealed class CSCViewState {
|
internal sealed class CSCViewState {
|
||||||
|
|
||||||
fun ensureConnectedState(): CSCViewConnectedState {
|
fun ensureConnectedState(): CSCViewConnectedState {
|
||||||
@@ -30,7 +28,7 @@ internal data class CSCViewConnectedState(
|
|||||||
val totalDistance: Float = 0f,
|
val totalDistance: Float = 0f,
|
||||||
val gearRatio: Float = 0f,
|
val gearRatio: Float = 0f,
|
||||||
val batteryLevel: Int = 0,
|
val batteryLevel: Int = 0,
|
||||||
val wheelSize: String = String.EMPTY
|
val wheelSize: String = CSCSettings.DefaultWheelSize.NAME
|
||||||
) : CSCViewState() {
|
) : CSCViewState() {
|
||||||
|
|
||||||
fun displaySpeed(): String {
|
fun displaySpeed(): String {
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.R
|
||||||
import no.nordicsemi.android.csc.service.CSCService
|
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
|
@Composable
|
||||||
internal fun CscScreen(navController: NavController, viewModel: CscViewModel = hiltViewModel()) {
|
internal fun CscScreen(navController: NavController, viewModel: CscViewModel = hiltViewModel()) {
|
||||||
@@ -37,11 +39,6 @@ internal fun CscScreen(navController: NavController, viewModel: CscViewModel = h
|
|||||||
secondScreenResult?.value?.let {
|
secondScreenResult?.value?.let {
|
||||||
viewModel.onEvent(OnBluetoothDeviceSelected(it))
|
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
|
navController.currentBackStackEntry
|
||||||
?.savedStateHandle
|
?.savedStateHandle
|
||||||
?.set("result", null)
|
?.set("result", null)
|
||||||
@@ -77,7 +74,14 @@ private fun NotConnectedScreen(
|
|||||||
onEvent(OnMovedToScannerScreen)
|
onEvent(OnMovedToScannerScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (LocalContext.current.isServiceRunning(CSCService::class.java.name)) {
|
||||||
|
val intent = Intent(LocalContext.current, CSCService::class.java)
|
||||||
|
LocalContext.current.stopService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
NotConnectedView(onEvent)
|
NotConnectedView(onEvent)
|
||||||
|
|
||||||
|
LocalContext.current.stopService(Intent(LocalContext.current, CSCService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -105,6 +109,11 @@ private fun ConnectedView(state: CSCViewConnectedState, onEvent: (CSCViewEvent)
|
|||||||
SelectWheelSizeDialog { onEvent(it) }
|
SelectWheelSizeDialog { onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!LocalContext.current.isServiceRunning(CSCService::class.java.name)) {
|
||||||
|
val intent = Intent(LocalContext.current, CSCService::class.java)
|
||||||
|
LocalContext.current.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
@@ -118,7 +127,6 @@ private fun ConnectedView(state: CSCViewConnectedState, onEvent: (CSCViewEvent)
|
|||||||
Button(onClick = { onEvent(OnDisconnectButtonClick) }) {
|
Button(onClick = { onEvent(OnDisconnectButtonClick) }) {
|
||||||
Text(text = stringResource(id = R.string.csc_disconnect))
|
Text(text = stringResource(id = R.string.csc_disconnect))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.csc.view
|
package no.nordicsemi.android.csc.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -8,27 +8,44 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import no.nordicsemi.android.broadcast.BluetoothDataReadBroadcast
|
import no.nordicsemi.android.csc.events.CSCServiceEvent
|
||||||
import no.nordicsemi.android.events.CSCServiceEvent
|
import no.nordicsemi.android.csc.events.CrankDataChanged
|
||||||
import no.nordicsemi.android.events.CrankDataChanged
|
import no.nordicsemi.android.csc.events.OnBatteryLevelChanged
|
||||||
import no.nordicsemi.android.events.OnBatteryLevelChanged
|
import no.nordicsemi.android.csc.events.OnDistanceChangedEvent
|
||||||
import no.nordicsemi.android.events.OnDistanceChangedEvent
|
import no.nordicsemi.android.csc.service.CSCDataReadBroadcast
|
||||||
import no.nordicsemi.android.events.exhaustive
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class CscViewModel @Inject constructor(
|
internal class CscViewModel @Inject constructor(
|
||||||
private val localBroadcast: BluetoothDataReadBroadcast
|
private val localBroadcast: CSCDataReadBroadcast,
|
||||||
|
private val deviceHolder: SelectedBluetoothDeviceHolder
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val state = MutableStateFlow<CSCViewState>(CSCViewNotConnectedState())
|
val state = MutableStateFlow(createInitialState())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
localBroadcast.events.onEach {
|
localBroadcast.events.onEach {
|
||||||
(it as? CSCServiceEvent)?.let { withContext(Dispatchers.Main) { consumeEvent(it) }}
|
withContext(Dispatchers.Main) { consumeEvent(it) }
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createInitialState(): CSCViewState {
|
||||||
|
return deviceHolder.device?.let { CSCViewConnectedState() } ?: CSCViewNotConnectedState()
|
||||||
|
}
|
||||||
|
|
||||||
private fun consumeEvent(event: CSCServiceEvent) {
|
private fun consumeEvent(event: CSCServiceEvent) {
|
||||||
val newValue = when (event) {
|
val newValue = when (event) {
|
||||||
is CrankDataChanged -> createNewState(event)
|
is CrankDataChanged -> createNewState(event)
|
||||||
@@ -64,7 +81,7 @@ internal class CscViewModel @Inject constructor(
|
|||||||
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
||||||
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
||||||
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
||||||
OnDisconnectButtonClick -> TODO()
|
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
||||||
OnConnectButtonClick -> onConnectButtonClick()
|
OnConnectButtonClick -> onConnectButtonClick()
|
||||||
OnMovedToScannerScreen -> onOnMovedToScannerScreen()
|
OnMovedToScannerScreen -> onOnMovedToScannerScreen()
|
||||||
is OnBluetoothDeviceSelected -> onBluetoothDeviceSelected()
|
is OnBluetoothDeviceSelected -> onBluetoothDeviceSelected()
|
||||||
@@ -87,6 +104,10 @@ internal class CscViewModel @Inject constructor(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onDisconnectButtonClick() {
|
||||||
|
state.tryEmit(CSCViewNotConnectedState())
|
||||||
|
}
|
||||||
|
|
||||||
private fun onConnectButtonClick() {
|
private fun onConnectButtonClick() {
|
||||||
state.tryEmit(state.value.ensureDisconnectedState().copy(showScannerDialog = true))
|
state.tryEmit(state.value.ensureDisconnectedState().copy(showScannerDialog = true))
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,10 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="csc_title">Cyclic and speed cadence</string>
|
<string name="csc_title">Cyclic and speed cadence</string>
|
||||||
|
|
||||||
<string name="csc_bonding">Bonding with the device…</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_disconnect">Disconnect</string>
|
||||||
<string name="csc_no_connection">No device connected</string>
|
<string name="csc_no_connection">No device connected</string>
|
||||||
<string name="csc_connect">Connect</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_speed">Speed</string>
|
||||||
<string name="scs_field_cadence">Cadence</string>
|
<string name="scs_field_cadence">Cadence</string>
|
||||||
<string name="scs_field_distance">Distance</string>
|
<string name="scs_field_distance">Distance</string>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ apply from: rootProject.file("library.gradle")
|
|||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(":lib_utils")
|
||||||
implementation project(":lib_theme")
|
implementation project(":lib_theme")
|
||||||
implementation project(":lib_events")
|
|
||||||
|
|
||||||
implementation libs.material
|
implementation libs.material
|
||||||
implementation libs.google.permissions
|
implementation libs.google.permissions
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,20 +8,17 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import no.nordicsemi.android.events.exhaustive
|
|
||||||
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
||||||
import no.nordicsemi.android.scanner.ui.BluetoothNotAvailableScreen
|
import no.nordicsemi.android.scanner.view.*
|
||||||
import no.nordicsemi.android.scanner.ui.BluetoothNotEnabledScreen
|
import no.nordicsemi.android.scanner.viewmodel.NordicBleScannerViewModel
|
||||||
import no.nordicsemi.android.scanner.ui.NordicBleScannerViewModel
|
import no.nordicsemi.android.scanner.viewmodel.ScannerViewEvent
|
||||||
import no.nordicsemi.android.scanner.ui.RequestPermissionScreen
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import no.nordicsemi.android.scanner.ui.ScanDeviceScreen
|
|
||||||
import no.nordicsemi.android.scanner.ui.ScannerViewEvent
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScannerRoute(navController: NavController) {
|
fun ScannerRoute(navController: NavController) {
|
||||||
val viewModel = hiltViewModel<NordicBleScannerViewModel>()
|
val viewModel = hiltViewModel<NordicBleScannerViewModel>()
|
||||||
|
|
||||||
val scannerStatus = viewModel.state.collectAsState().value.scannerStatus
|
val scannerStatus = viewModel.state.collectAsState().value
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__devices_list)) })
|
TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__devices_list)) })
|
||||||
@@ -3,8 +3,6 @@ package no.nordicsemi.android.scanner.tools
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.le.ScanCallback
|
|
||||||
import android.bluetooth.le.ScanResult
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -13,22 +11,6 @@ internal class NordicBleScanner @Inject constructor(private val bleAdapter: Blue
|
|||||||
|
|
||||||
val scannerResult = MutableStateFlow<ScanningResult>(DeviceListResult())
|
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 {
|
fun getBluetoothStatus(): ScannerStatus {
|
||||||
return when {
|
return when {
|
||||||
bleAdapter == null -> ScannerStatus.NOT_AVAILABLE
|
bleAdapter == null -> ScannerStatus.NOT_AVAILABLE
|
||||||
@@ -36,22 +18,6 @@ internal class NordicBleScanner @Inject constructor(private val bleAdapter: Blue
|
|||||||
else -> ScannerStatus.DISABLED
|
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
|
sealed class ScanningResult
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
package no.nordicsemi.android.scanner.tools
|
package no.nordicsemi.android.scanner.tools
|
||||||
|
|
||||||
enum class ScannerStatus {
|
internal enum class ScannerStatus {
|
||||||
PERMISSION_REQUIRED, ENABLED, DISABLED, NOT_AVAILABLE
|
PERMISSION_REQUIRED, ENABLED, DISABLED, NOT_AVAILABLE
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.ui
|
package no.nordicsemi.android.scanner.view
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
package no.nordicsemi.android.scanner.ui
|
package no.nordicsemi.android.scanner.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
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.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package no.nordicsemi.android.scanner.ui
|
package no.nordicsemi.android.scanner.view
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
import android.companion.AssociationRequest
|
import android.companion.AssociationRequest
|
||||||
import android.companion.BluetoothDeviceFilter
|
import android.companion.BluetoothLeDeviceFilter
|
||||||
import android.companion.CompanionDeviceManager
|
import android.companion.CompanionDeviceManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.IntentSender
|
import android.content.IntentSender
|
||||||
@@ -20,10 +21,11 @@ import androidx.navigation.NavController
|
|||||||
@Composable
|
@Composable
|
||||||
fun ScanDeviceScreen(navController: NavController,) {
|
fun ScanDeviceScreen(navController: NavController,) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
|
val deviceFilter = BluetoothLeDeviceFilter.Builder()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
|
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
|
||||||
|
.addDeviceFilter(deviceFilter)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val deviceManager =
|
val deviceManager =
|
||||||
@@ -32,13 +34,18 @@ fun ScanDeviceScreen(navController: NavController,) {
|
|||||||
val contract = ActivityResultContracts.StartIntentSenderForResult()
|
val contract = ActivityResultContracts.StartIntentSenderForResult()
|
||||||
val launcher = rememberLauncherForActivityResult(contract = contract, onResult = {
|
val launcher = rememberLauncherForActivityResult(contract = contract, onResult = {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
val deviceToPair: BluetoothDevice? = it.data?.getParcelableExtra(
|
//Sometimes result is ScanResult & sometimes BluetoothDevice
|
||||||
CompanionDeviceManager.EXTRA_DEVICE)
|
val device: BluetoothDevice = try {
|
||||||
|
it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)!!
|
||||||
|
} catch (e: Exception) {
|
||||||
|
(it.data?.getParcelableExtra<ScanResult>(CompanionDeviceManager.EXTRA_DEVICE))!!.device
|
||||||
|
}
|
||||||
|
|
||||||
navController.previousBackStackEntry
|
navController.previousBackStackEntry
|
||||||
?.savedStateHandle
|
?.savedStateHandle
|
||||||
?.set("result", deviceToPair)
|
?.set("result", device)
|
||||||
navController.popBackStack()
|
|
||||||
}
|
}
|
||||||
|
navController.popBackStack()
|
||||||
})
|
})
|
||||||
|
|
||||||
val hasBeenInvoked = remember { mutableStateOf(false) }
|
val hasBeenInvoked = remember { mutableStateOf(false) }
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package no.nordicsemi.android.scanner.ui
|
package no.nordicsemi.android.scanner.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import no.nordicsemi.android.events.exhaustive
|
|
||||||
import no.nordicsemi.android.scanner.tools.NordicBleScanner
|
import no.nordicsemi.android.scanner.tools.NordicBleScanner
|
||||||
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -14,9 +14,7 @@ internal class NordicBleScannerViewModel @Inject constructor(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val state =
|
val state =
|
||||||
MutableStateFlow(NordicBleScannerState(scannerStatus = ScannerStatus.PERMISSION_REQUIRED))
|
MutableStateFlow(ScannerStatus.PERMISSION_REQUIRED)
|
||||||
|
|
||||||
val scannerResult = bleScanner.scannerResult
|
|
||||||
|
|
||||||
fun onEvent(event: ScannerViewEvent) {
|
fun onEvent(event: ScannerViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -26,19 +24,14 @@ internal class NordicBleScannerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onPermissionChecked() {
|
private fun onPermissionChecked() {
|
||||||
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
|
state.value = bleScanner.getBluetoothStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBluetoothEnabled() {
|
private fun onBluetoothEnabled() {
|
||||||
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
|
state.value = bleScanner.getBluetoothStatus()
|
||||||
bleScanner.startScanning()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ScannerViewEvent {
|
internal enum class ScannerViewEvent {
|
||||||
PERMISSION_CHECKED, BLUETOOTH_ENABLED
|
PERMISSION_CHECKED, BLUETOOTH_ENABLED
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class NordicBleScannerState(
|
|
||||||
val scannerStatus: ScannerStatus
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
apply from: rootProject.file("library.gradle")
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(":lib_events")
|
|
||||||
|
|
||||||
implementation libs.kotlin.coroutines
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="no.nordicsemi.android.broadcast">
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
apply from: rootProject.file("library.gradle")
|
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'kotlin-parcelize'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package no.nordicsemi.android.events
|
|
||||||
|
|
||||||
sealed class BluetoothReadDataEvent
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package no.nordicsemi.android.events
|
|
||||||
|
|
||||||
val <T> T.exhaustive
|
|
||||||
get() = this
|
|
||||||
|
|
||||||
val String.Companion.EMPTY
|
|
||||||
get() = ""
|
|
||||||
@@ -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
18
lib_service/build.gradle
Normal 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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.broadcast
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("no.nordicsemi.android.broadcast.test", appContext.packageName)
|
assertEquals("no.nordicsemi.android.service.test", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5
lib_service/src/main/AndroidManifest.xml
Normal file
5
lib_service/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.csc.batery
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothGatt
|
import android.bluetooth.BluetoothGatt
|
||||||
@@ -9,7 +9,6 @@ import androidx.annotation.IntRange
|
|||||||
import no.nordicsemi.android.ble.callback.DataReceivedCallback
|
import no.nordicsemi.android.ble.callback.DataReceivedCallback
|
||||||
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
|
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
import no.nordicsemi.android.csc.service.BatteryManagerCallbacks
|
|
||||||
import no.nordicsemi.android.log.LogContract
|
import no.nordicsemi.android.log.LogContract
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
|
|||||||
) {
|
) {
|
||||||
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
|
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
|
||||||
this@BatteryManager.batteryLevel = batteryLevel
|
this@BatteryManager.batteryLevel = batteryLevel
|
||||||
mCallbacks!!.onBatteryLevelChanged(device, batteryLevel)
|
mCallbacks?.onBatteryLevelChanged(device, batteryLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
|
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
|
||||||
@@ -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.BleManagerCallbacks
|
||||||
import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback
|
import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.csc.batery
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -13,17 +13,7 @@ import no.nordicsemi.android.log.Logger
|
|||||||
*
|
*
|
||||||
* @param <T> the callbacks class.
|
* @param <T> the callbacks class.
|
||||||
</T> */
|
</T> */
|
||||||
abstract class LoggableBleManager<T : BleManagerCallbacks?>
|
abstract class LoggableBleManager<T : BleManagerCallbacks?>(context: Context) : LegacyBleManager<T>(context) {
|
||||||
/**
|
|
||||||
* The manager constructor.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* After constructing the manager, the callbacks object must be set with
|
|
||||||
* [.setGattCallbacks].
|
|
||||||
*
|
|
||||||
* @param context the context.
|
|
||||||
*/
|
|
||||||
(context: Context) : LegacyBleManager<T>(context) {
|
|
||||||
private var logSession: ILogSession? = null
|
private var logSession: ILogSession? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
7
lib_service/src/main/res/values/strings.xml
Normal file
7
lib_service/src/main/res/values/strings.xml
Normal 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…</string>
|
||||||
|
<string name="csc_bonded">The device is now bonded.</string>
|
||||||
|
<string name="csc_notification_connected_message">%s is connected.</string>
|
||||||
|
</resources>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.broadcast
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
2
lib_utils/build.gradle
Normal file
2
lib_utils/build.gradle
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
apply from: rootProject.file("library.gradle")
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.events
|
package no.nordicsemi.android.utils
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("no.nordicsemi.android.events.test", appContext.packageName)
|
assertEquals("no.nordicsemi.android.utils.test", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="no.nordicsemi.android.events">
|
package="no.nordicsemi.android.utils">
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
16
lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt
Normal file
16
lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.events
|
package no.nordicsemi.android.utils
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ dependencyResolutionManagement {
|
|||||||
alias('nordic-log').to('no.nordicsemi.android:log:2.3.0')
|
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('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')
|
alias('material').to('com.google.android.material:material:1.4.0')
|
||||||
|
|
||||||
version('lifecycle', '2.3.1')
|
version('lifecycle', '2.3.1')
|
||||||
@@ -57,12 +58,14 @@ dependencyResolutionManagement {
|
|||||||
rootProject.name = "Android-nRF-Toolbox"
|
rootProject.name = "Android-nRF-Toolbox"
|
||||||
|
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|
||||||
include ':feature_csc'
|
include ':feature_csc'
|
||||||
include ':lib_broadcast'
|
include ':feature_scanner'
|
||||||
include ':lib_events'
|
|
||||||
|
include ':lib_service'
|
||||||
|
include ':lib_theme'
|
||||||
|
include ':lib_utils'
|
||||||
|
|
||||||
if (file('../Android-BLE-Library').exists()) {
|
if (file('../Android-BLE-Library').exists()) {
|
||||||
includeBuild('../Android-BLE-Library')
|
includeBuild('../Android-BLE-Library')
|
||||||
}
|
}
|
||||||
include ':lib_theme'
|
|
||||||
include ':lib_scanner'
|
|
||||||
|
|||||||
Reference in New Issue
Block a user