Commit 2203d336 authored by stopcovid@lunabee.com's avatar stopcovid@lunabee.com
Browse files

Update to 2.2.4

- Update BLE to 2.1.0
- Enhance concurrency security around file writing
- Add share to graph
parent 241fa207
......@@ -19,7 +19,7 @@ android {
minSdkVersion 21
targetSdkVersion 30
buildConfigField 'String', 'BLE_VERSION', '"1.4.0"'
buildConfigField 'String', 'BLE_VERSION', '"2.1.0"'
}
compileOptions {
......
......@@ -17,6 +17,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-feature
android:name="android.hardware.bluetooth_le"
......
......@@ -15,12 +15,12 @@ interface ProximityNotification {
fun setUp(
proximityPayloadProvider: ProximityPayloadProvider,
proximityPayloadIdProvider: ProximityPayloadIdProvider,
callback: ProximityNotificationCallback
)
fun start()
suspend fun start()
suspend fun stop()
fun stop()
fun notifyPayloadUpdated()
suspend fun notifyPayloadUpdated(proximityPayload: ProximityPayload)
}
......@@ -11,6 +11,6 @@
package com.orange.proximitynotification
interface ProximityNotificationCallback {
fun onProximity(proximityInfo: ProximityInfo)
fun onError(error: ProximityNotificationError)
suspend fun onProximity(proximityInfo: ProximityInfo)
suspend fun onError(error: ProximityNotificationError)
}
\ No newline at end of file
......@@ -10,7 +10,19 @@
package com.orange.proximitynotification
data class ProximityNotificationError(val type: Type, val rootErrorCode: Int? = null, val cause: String? = null) {
data class ProximityNotificationError(
val type: Type,
val rootErrorCode: Int? = null,
val cause: String? = null
) {
companion object {
/**
* Root error code when too much BLE operation fail which may indicate that Bluetooth Stack
* is unhealthy.
*/
const val UNHEALTHY_BLUETOOTH_ERROR_CODE = 1000
}
enum class Type {
/**
......@@ -27,10 +39,5 @@ data class ProximityNotificationError(val type: Type, val rootErrorCode: Int? =
* BLE gatt error
*/
BLE_GATT,
/**
* BLE proximity notification component error
*/
BLE_PROXIMITY_NOTIFICATION
}
}
\ No newline at end of file
......@@ -10,7 +10,10 @@
package com.orange.proximitynotification
import com.orange.proximitynotification.ProximityNotificationEventId.Category.*
import com.orange.proximitynotification.ProximityNotificationEventId.Category.BLE_ADVERTISER
import com.orange.proximitynotification.ProximityNotificationEventId.Category.BLE_GATT
import com.orange.proximitynotification.ProximityNotificationEventId.Category.BLE_SCANNER
import com.orange.proximitynotification.ProximityNotificationEventId.Category.PROXIMITY_NOTIFICATION
enum class ProximityNotificationEventId(val category: Category) {
......@@ -27,10 +30,16 @@ enum class ProximityNotificationEventId(val category: Category) {
BLE_GATT_STOP(BLE_GATT),
BLE_GATT_STOP_ERROR(BLE_GATT),
BLE_GATT_STOP_SUCCESS(BLE_GATT),
BLE_GATT_CONNECT_ERROR(BLE_GATT),
BLE_GATT_CONNECT_SUCCESS(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI_TIMEOUT(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI_ERROR(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI_SUCCESS(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD_TIMEOUT(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD_ERROR(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD_SUCCESS(BLE_GATT),
BLE_GATT_ON_CHARACTERISTIC_WRITE_REQUEST(BLE_GATT),
BLE_GATT_ON_CHARACTERISTIC_WRITE_REQUEST_ERROR(BLE_GATT),
BLE_GATT_ON_CHARACTERISTIC_WRITE_REQUEST_SUCCESS(BLE_GATT),
......@@ -52,9 +61,13 @@ enum class ProximityNotificationEventId(val category: Category) {
PROXIMITY_NOTIFICATION_BLE_SETTINGS_UPDATED(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_BLUETOOTH_DISABLED(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_BLUETOOTH_ENABLED(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH(PROXIMITY_NOTIFICATION),
BLE_PROXIMITY_NOTIFICATION_WITHOUT_ADVERTISER(PROXIMITY_NOTIFICATION),
BLE_PROXIMITY_NOTIFICATION_FACTORY(PROXIMITY_NOTIFICATION);
enum class Category {
/**
* BLE advertising error
......@@ -77,4 +90,5 @@ enum class ProximityNotificationEventId(val category: Category) {
PROXIMITY_NOTIFICATION
}
}
\ No newline at end of file
......@@ -22,27 +22,39 @@ import com.orange.proximitynotification.ble.BleSettings
import com.orange.proximitynotification.tools.BluetoothStateBroadcastReceiver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import timber.log.Timber
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.CoroutineContext
/**
* ProximityNotification foreground service.
*/
abstract class ProximityNotificationService : Service(),
ProximityNotificationCallback, ProximityPayloadProvider, ProximityNotificationLogger.Listener,
ProximityNotificationCallback, ProximityPayloadProvider, ProximityPayloadIdProvider,
ProximityNotificationLogger.Listener,
CoroutineScope {
private var bleProximityNotification: BleProximityNotification? = null
private var bluetoothStateBroadcastReceiver: BluetoothStateBroadcastReceiver? = null
private val bluetoothRestartInProgress = AtomicBoolean(false)
val isBluetoothRestartInProgress: Boolean
get() = bluetoothRestartInProgress.get()
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + SupervisorJob()
get() = Dispatchers.Default + job
abstract val foregroundNotificationId: Int
abstract val bleSettings: BleSettings
private val bluetoothAdapter: BluetoothAdapter
get() = BluetoothAdapter.getDefaultAdapter()
override fun onCreate() {
super.onCreate()
ProximityNotificationLogger.registerListener(this)
......@@ -53,7 +65,7 @@ abstract class ProximityNotificationService : Service(),
ProximityNotificationLogger.unregisterListener()
super.onDestroy()
cancel()
job.cancel()
}
override fun onBind(intent: Intent?): IBinder? {
......@@ -63,7 +75,7 @@ abstract class ProximityNotificationService : Service(),
/**
* Starts ProximityNotification and foreground service.
*
* It registers a BluetoothStateBroadcastReceiver in order to be notified of state changes
* It registers a [BluetoothStateBroadcastReceiver] in order to be notified of state changes
* @see BluetoothStateBroadcastReceiver
*/
fun start() {
......@@ -79,7 +91,7 @@ abstract class ProximityNotificationService : Service(),
/**
* Stops ProximityNotification and foreground service.
*
* It unregisters a BluetoothStateBroadcastReceiver in order to be notified of state changes
* It unregisters [BluetoothStateBroadcastReceiver] if any registered
* @see BluetoothStateBroadcastReceiver
*/
fun stop() {
......@@ -93,24 +105,26 @@ abstract class ProximityNotificationService : Service(),
}
/**
* Notify ProximityPayload provided by ProximityPayloadProvider has changed.
* Notify that [ProximityPayload] provided by [ProximityPayloadProvider] has changed.
*
* @param proximityPayload Updated ProximityPayload
* @see ProximityPayloadProvider
*/
fun notifyProximityPayloadUpdated() {
suspend fun notifyProximityPayloadUpdated(proximityPayload: ProximityPayload) {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_PAYLOAD_UPDATED,
"Proximity payload updated"
)
bleProximityNotification?.notifyPayloadUpdated()
bleProximityNotification?.notifyPayloadUpdated(proximityPayload)
}
/**
* Notify that [BleSettings] have changed
* It will restart [BleProximityNotification]
* Notify that [BleSettings] have changed.
* It will restart [BleProximityNotification] if it is running
*
*/
fun notifyBleSettingsUpdate() {
suspend fun notifyBleSettingsUpdate() {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_BLE_SETTINGS_UPDATED,
"BLE settings updated"
......@@ -122,22 +136,53 @@ abstract class ProximityNotificationService : Service(),
}
}
/**
* Force [BleProximityNotification] to restart.
*
* If will restart Bluetooth stack if needed by [BleProximityNotification].
* Otherwise it will restart [BleProximityNotification] by calling notifyBleSettingsUpdate
*/
suspend fun restart() {
if (isRunning()) {
if (bleProximityNotification?.shouldRestartBluetooth == true) {
restartBluetooth()
} else {
notifyBleSettingsUpdate()
}
}
}
/**
* @return true if ProximityNotification is running, false otherwise
*/
fun isRunning() = bleProximityNotification?.isRunning == true
protected open fun doStart() {
startForeground(foregroundNotificationId, buildForegroundServiceNotification())
startBleProximityNotification()
launch(Dispatchers.Main.immediate + NonCancellable) {
if (!isBluetoothRestartInProgress) {
startForeground(foregroundNotificationId, buildForegroundServiceNotification())
}
startBleProximityNotification()
}
}
protected open fun doStop() {
stopBleProximityNotification()
stopForeground(true)
launch(Dispatchers.Main.immediate + NonCancellable) {
stopBleProximityNotification()
if (!isBluetoothRestartInProgress) {
stopForeground(true)
}
}
}
private fun registerBluetoothBroadcastReceiver() {
bluetoothRestartInProgress.set(false)
bluetoothStateBroadcastReceiver = BluetoothStateBroadcastReceiver(
onBluetoothDisabled = { onBluetoothDisabled() },
onBluetoothEnabled = { onBluetoothEnabled() })
......@@ -154,9 +199,10 @@ abstract class ProximityNotificationService : Service(),
applicationContext.unregisterReceiver(it)
}
bluetoothStateBroadcastReceiver = null
bluetoothRestartInProgress.set(false)
}
private fun startBleProximityNotification() {
private suspend fun startBleProximityNotification() {
if (bleProximityNotification != null) {
stopBleProximityNotification()
}
......@@ -166,21 +212,18 @@ abstract class ProximityNotificationService : Service(),
"Start BLE Proximity Notification"
)
BleProximityNotificationFactory.build(this, bleSettings, this)?.let {
bleProximityNotification = it
it.setUp(this, this)
it.start()
} ?: run {
onError(
ProximityNotificationError(
ProximityNotificationError.Type.BLE_PROXIMITY_NOTIFICATION,
cause = "Failed to initialize bluetooth"
bleProximityNotification =
BleProximityNotificationFactory.build(this, bleSettings, this).apply {
setUp(
this@ProximityNotificationService,
this@ProximityNotificationService,
this@ProximityNotificationService
)
)
}
start()
}
}
private fun stopBleProximityNotification() {
private suspend fun stopBleProximityNotification() {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_STOP_BLE,
"Stop BLE Proximity Notification"
......@@ -190,6 +233,45 @@ abstract class ProximityNotificationService : Service(),
bleProximityNotification = null
}
/**
* To restart the Bluetooth stack.
* Beware, on some devices it could show a confirmation popup while disabling / enabling
* Bluetooth.
*/
protected suspend fun restartBluetooth() {
withContext(Dispatchers.Main) {
if (bluetoothRestartInProgress.getAndSet(true)) {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH,
"Restart Bluetooth - already in progress"
)
}
when {
!bluetoothAdapter.isEnabled -> {
val status = bluetoothAdapter.enable()
bluetoothRestartInProgress.set(status)
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH,
"Restart Bluetooth - enabling (status=$status)"
)
}
else -> {
val status = bluetoothAdapter.disable()
bluetoothRestartInProgress.set(status)
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH,
"Restart Bluetooth - disabling (status=$status)"
)
}
}
}
}
/**
* Called once Bluetooth is disabled.
* By default it stops the ProximityNotification and the foreground service
......@@ -203,6 +285,23 @@ abstract class ProximityNotificationService : Service(),
)
doStop()
if (isBluetoothRestartInProgress) {
launch {
delay(1000)
withContext(Dispatchers.Main) {
val status = bluetoothAdapter.enable()
bluetoothAdapter.cancelDiscovery()
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH,
"Restart Bluetooth - enabling on Bluetooth disabled (status=$status)"
)
bluetoothRestartInProgress.set(status)
}
}
}
}
/**
......@@ -218,10 +317,18 @@ abstract class ProximityNotificationService : Service(),
)
doStart()
if (bluetoothRestartInProgress.getAndSet(false)) {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH,
"Restart Bluetooth - restart bluetooth done"
)
}
}
override fun onEvent(event: ProximityNotificationEvent) {
Timber.v("onEvent event = $event")
}
/**
......
......@@ -38,4 +38,5 @@ data class ProximityPayload(val data: ByteArray) {
return data.contentHashCode()
}
}
\ No newline at end of file
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Orange / Date - 2020/04/27 - for the TOUS-ANTI-COVID project
*/
package com.orange.proximitynotification
/**
* [ProximityPayload] identifier
*/
typealias ProximityPayloadId = ByteArray
/**
* Provides the [ProximityPayloadId]
*
* @see ProximityPayloadId
*/
interface ProximityPayloadIdProvider {
/**
* Returns the current [ProximityPayloadId] for a [ProximityPayload]
*
* @param proximityPayload [ProximityPayload] from which [ProximityPayloadId] should be extracted
* @return [ProximityPayloadId] extracted or null if not found
*/
suspend fun fromProximityPayload(proximityPayload: ProximityPayload): ProximityPayloadId?
}
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Orange / Date - 2020/12/18 - for the TOUS-ANTI-COVID project
*/
package com.orange.proximitynotification
import com.orange.proximitynotification.tools.ExpiringCache
/**
* [ProximityPayloadIdProvider] wrapper implementation using [ExpiringCache]
*
* @param proximityPayloadIdProvider wrapped [ProximityPayloadIdProvider]
* @param maxSize [ExpiringCache] maxSize
* @param expiringTime [ExpiringCache] expiringTime
*/
internal class ProximityPayloadIdProviderWithCache(
private val proximityPayloadIdProvider: ProximityPayloadIdProvider,
maxSize: Int,
expiringTime: Long
) : ProximityPayloadIdProvider {
private val cache = ExpiringCache<ProximityPayload, ProximityPayloadId>(maxSize, expiringTime)
override suspend fun fromProximityPayload(proximityPayload: ProximityPayload): ProximityPayloadId? {
cache[proximityPayload]?.let {
return it
}
val proximityPayloadId = proximityPayloadIdProvider.fromProximityPayload(proximityPayload)
proximityPayloadId?.let {
cache.put(proximityPayload, proximityPayloadId)
}
return proximityPayloadId
}
}
......@@ -11,16 +11,16 @@
package com.orange.proximitynotification
/**
* Provides the ProximityPayload to exchange
* Provides the [ProximityPayload] to exchange
*
* @see ProximityPayload
*/
interface ProximityPayloadProvider {
/**
* Return the current ProximityPayload
* Return the current [ProximityPayload]
*
* @return ProximityPayload to exchange
* @return [ProximityPayload] to exchange
*/
suspend fun current(): ProximityPayload
}
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Orange / Date - 2020/11/04 - for the TOUS-ANTI-COVID project
*/
package com.orange.proximitynotification.ble
import android.bluetooth.BluetoothDevice
import com.orange.proximitynotification.ble.scanner.BleScannedDevice
internal typealias DeviceId = String
internal fun BleScannedDevice.deviceId(): DeviceId = device.deviceId()
internal fun BluetoothDevice.deviceId(): DeviceId = address
\ No newline at end of file
......@@ -14,21 +14,27 @@ import com.orange.proximitynotification.ProximityPayload
data class BlePayload(
val proximityPayload: ProximityPayload,
val version: Int,
val txPowerLevel: Int
val version: Int = VERSION,
val txPowerLevel: Int,
val calibratedRssi: Int? = null
) {
companion object {
const val VERSION = 2
private const val PROXIMITY_PAYLOAD_OFFSET = 0
private const val PROXIMITY_PAYLOAD_SIZE = ProximityPayload.SIZE
private const val VERSION_OFFSET = PROXIMITY_PAYLOAD_OFFSET + PROXIMITY_PAYLOAD_SIZE
private const val VERSION_SIZE = 1
private const val TX_POWER_LEVEL_OFFSET = VERSION_OFFSET + 1
private const val TX_POWER_LEVEL_SIZE = 1
private const val CALIBRATED_RSSI_OFFSET = TX_POWER_LEVEL_OFFSET + 1
private const val CALIBRATED_RSSI_SIZE = 1
private const val SIZE = PROXIMITY_PAYLOAD_SIZE + VERSION_SIZE + TX_POWER_LEVEL_SIZE
fun from(data: ByteArray): BlePayload {
require(data.size >= SIZE) { "Expecting a byte array of $SIZE bytes. Got ${data.size}." }
......@@ -41,7 +47,8 @@ data class BlePayload(
)
),
version = data[VERSION_OFFSET].toInt(),
txPowerLevel = data[TX_POWER_LEVEL_OFFSET].toInt()
txPowerLevel = data[TX_POWER_LEVEL_OFFSET].toInt(),
calibratedRssi = data.getOrNull(CALIBRATED_RSSI_OFFSET)?.toInt()
)
}
......@@ -55,11 +62,18 @@ data class BlePayload(