mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 15:34:26 +01:00
Replace CompanionDeviceManager with Scanner library.
This commit is contained in:
@@ -54,6 +54,7 @@ dependencies {
|
|||||||
implementation project(':profile_hrs')
|
implementation project(':profile_hrs')
|
||||||
implementation project(':profile_hts')
|
implementation project(':profile_hts')
|
||||||
implementation project(':profile_gls')
|
implementation project(':profile_gls')
|
||||||
|
implementation project(':profile_permission')
|
||||||
implementation project(':profile_scanner')
|
implementation project(':profile_scanner')
|
||||||
implementation project(":lib_theme")
|
implementation project(":lib_theme")
|
||||||
implementation project(":lib_utils")
|
implementation project(":lib_utils")
|
||||||
|
|||||||
@@ -20,16 +20,18 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
import no.nordicsemi.android.csc.view.CSCScreen
|
import no.nordicsemi.android.csc.view.CSCScreen
|
||||||
import no.nordicsemi.android.gls.view.GLSScreen
|
import no.nordicsemi.android.gls.view.GLSScreen
|
||||||
import no.nordicsemi.android.hrs.view.HRSScreen
|
import no.nordicsemi.android.hrs.view.HRSScreen
|
||||||
import no.nordicsemi.android.hts.view.HTSScreen
|
import no.nordicsemi.android.hts.view.HTSScreen
|
||||||
import no.nordicsemi.android.scanner.view.BluetoothNotAvailableScreen
|
import no.nordicsemi.android.permission.view.BluetoothNotAvailableScreen
|
||||||
import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen
|
import no.nordicsemi.android.permission.view.BluetoothNotEnabledScreen
|
||||||
import no.nordicsemi.android.scanner.view.RequestPermissionScreen
|
import no.nordicsemi.android.permission.view.RequestPermissionScreen
|
||||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreen
|
import no.nordicsemi.android.scanner.view.ScanDeviceScreen
|
||||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
||||||
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
||||||
@@ -56,10 +58,13 @@ internal fun HomeScreen() {
|
|||||||
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
||||||
BluetoothNotEnabledScreen(continueAction)
|
BluetoothNotEnabledScreen(continueAction)
|
||||||
}
|
}
|
||||||
composable(NavDestination.DEVICE_NOT_CONNECTED.id) {
|
composable(
|
||||||
ScanDeviceScreen {
|
NavDestination.DEVICE_NOT_CONNECTED.id,
|
||||||
|
arguments = listOf(navArgument("args") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
ScanDeviceScreen(it.arguments?.getString(ARGS_KEY)!!) {
|
||||||
when (it) {
|
when (it) {
|
||||||
ScanDeviceScreenResult.SUCCESS -> viewModel.finish()
|
ScanDeviceScreenResult.OK -> viewModel.finish()
|
||||||
ScanDeviceScreenResult.CANCEL -> viewModel.navigateUp()
|
ScanDeviceScreenResult.CANCEL -> viewModel.navigateUp()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
@@ -67,7 +72,7 @@ internal fun HomeScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state) {
|
LaunchedEffect(state) {
|
||||||
navController.navigate(state.id)
|
navController.navigate(state.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
|
const val ARGS_KEY = "args"
|
||||||
|
|
||||||
enum class NavDestination(val id: String) {
|
enum class NavDestination(val id: String) {
|
||||||
HOME("home-screen"),
|
HOME("home-screen"),
|
||||||
CSC("csc-screen"),
|
CSC("csc-screen"),
|
||||||
@@ -9,5 +11,5 @@ enum class NavDestination(val id: String) {
|
|||||||
REQUEST_PERMISSION("request-permission"),
|
REQUEST_PERMISSION("request-permission"),
|
||||||
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
|
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
|
||||||
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"),
|
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"),
|
||||||
DEVICE_NOT_CONNECTED("device-not-connected"),
|
DEVICE_NOT_CONNECTED("device-not-connected/{$ARGS_KEY}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
|
data class NavigationTarget(val destination: NavDestination, val args: String? = null) {
|
||||||
|
|
||||||
|
val url: String = args?.let { destination.id.replace("{$ARGS_KEY}", it) } ?: destination.id
|
||||||
|
}
|
||||||
@@ -3,11 +3,15 @@ package no.nordicsemi.android.nrftoolbox
|
|||||||
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.scanner.tools.NordicBleScanner
|
import no.nordicsemi.android.csc.service.CYCLING_SPEED_AND_CADENCE_SERVICE_UUID
|
||||||
import no.nordicsemi.android.scanner.tools.PermissionHelper
|
import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID
|
||||||
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
import no.nordicsemi.android.hrs.service.HR_SERVICE_UUID
|
||||||
|
import no.nordicsemi.android.hts.service.HT_SERVICE_UUID
|
||||||
|
import no.nordicsemi.android.permission.tools.NordicBleScanner
|
||||||
|
import no.nordicsemi.android.permission.tools.PermissionHelper
|
||||||
|
import no.nordicsemi.android.permission.tools.ScannerStatus
|
||||||
|
import no.nordicsemi.android.permission.viewmodel.BluetoothPermissionState
|
||||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||||
import no.nordicsemi.android.scanner.viewmodel.BluetoothPermissionState
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -17,7 +21,7 @@ class NavigationViewModel @Inject constructor(
|
|||||||
private val selectedDevice: SelectedBluetoothDeviceHolder
|
private val selectedDevice: SelectedBluetoothDeviceHolder
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
val state= MutableStateFlow(NavDestination.HOME)
|
val state= MutableStateFlow(NavigationTarget(NavDestination.HOME))
|
||||||
private var targetDestination = NavDestination.HOME
|
private var targetDestination = NavDestination.HOME
|
||||||
|
|
||||||
fun navigate(destination: NavDestination) {
|
fun navigate(destination: NavDestination) {
|
||||||
@@ -27,11 +31,11 @@ class NavigationViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun navigateUp() {
|
fun navigateUp() {
|
||||||
targetDestination = NavDestination.HOME
|
targetDestination = NavDestination.HOME
|
||||||
state.value = NavDestination.HOME
|
state.value = NavigationTarget(NavDestination.HOME)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finish() {
|
fun finish() {
|
||||||
if (state.value != targetDestination) {
|
if (state.value.destination != targetDestination) {
|
||||||
navigateToNextScreen()
|
navigateToNextScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,12 +51,33 @@ class NavigationViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToNextScreen() {
|
private fun navigateToNextScreen() {
|
||||||
state.value = when (getBluetoothState()) {
|
val destination = when (getBluetoothState()) {
|
||||||
BluetoothPermissionState.PERMISSION_REQUIRED -> NavDestination.REQUEST_PERMISSION
|
BluetoothPermissionState.PERMISSION_REQUIRED -> NavDestination.REQUEST_PERMISSION
|
||||||
BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE -> NavDestination.BLUETOOTH_NOT_AVAILABLE
|
BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE -> NavDestination.BLUETOOTH_NOT_AVAILABLE
|
||||||
BluetoothPermissionState.BLUETOOTH_NOT_ENABLED -> NavDestination.BLUETOOTH_NOT_ENABLED
|
BluetoothPermissionState.BLUETOOTH_NOT_ENABLED -> NavDestination.BLUETOOTH_NOT_ENABLED
|
||||||
BluetoothPermissionState.DEVICE_NOT_CONNECTED -> NavDestination.DEVICE_NOT_CONNECTED
|
BluetoothPermissionState.DEVICE_NOT_CONNECTED -> NavDestination.DEVICE_NOT_CONNECTED
|
||||||
BluetoothPermissionState.READY -> targetDestination
|
BluetoothPermissionState.READY -> targetDestination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val args = if (destination == NavDestination.DEVICE_NOT_CONNECTED) {
|
||||||
|
createServiceId(targetDestination)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
state.tryEmit(NavigationTarget(destination, args))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createServiceId(destination: NavDestination): String {
|
||||||
|
return when (destination) {
|
||||||
|
NavDestination.CSC -> CYCLING_SPEED_AND_CADENCE_SERVICE_UUID.toString()
|
||||||
|
NavDestination.HRS -> HR_SERVICE_UUID.toString()
|
||||||
|
NavDestination.HTS -> HT_SERVICE_UUID.toString()
|
||||||
|
NavDestination.GLS -> GLS_SERVICE_UUID.toString()
|
||||||
|
NavDestination.HOME,
|
||||||
|
NavDestination.REQUEST_PERMISSION,
|
||||||
|
NavDestination.BLUETOOTH_NOT_AVAILABLE,
|
||||||
|
NavDestination.BLUETOOTH_NOT_ENABLED,
|
||||||
|
NavDestination.DEVICE_NOT_CONNECTED -> throw IllegalArgumentException("There is no serivce related to the destination: $destination")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
package no.nordicsemi.android.service
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.companion.CompanionDeviceManager
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
class SelectedBluetoothDeviceHolder constructor(
|
class SelectedBluetoothDeviceHolder {
|
||||||
private val context: Context,
|
|
||||||
private val bluetoothAdapter: BluetoothAdapter?
|
|
||||||
) {
|
|
||||||
|
|
||||||
val device: BluetoothDevice?
|
var device: BluetoothDevice? = null
|
||||||
get() {
|
private set
|
||||||
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
|
||||||
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isBondingRequired(): Boolean {
|
fun isBondingRequired(): Boolean {
|
||||||
return device?.bondState == BluetoothDevice.BOND_NONE
|
return device?.bondState == BluetoothDevice.BOND_NONE
|
||||||
@@ -23,10 +14,11 @@ class SelectedBluetoothDeviceHolder constructor(
|
|||||||
device?.createBond()
|
device?.createBond()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun attachDevice(newDevice: BluetoothDevice) {
|
||||||
|
device = newDevice
|
||||||
|
}
|
||||||
|
|
||||||
fun forgetDevice() {
|
fun forgetDevice() {
|
||||||
device?.let {
|
device = null
|
||||||
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
|
||||||
deviceManager.disassociate(it.address)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> SpeedUnitRadioGroup(
|
fun <T> SelectItemRadioGroup(
|
||||||
currentItem: T,
|
currentItem: T,
|
||||||
items: List<RadioGroupItem<T>>,
|
items: List<RadioGroupItem<T>>,
|
||||||
onEvent: (RadioGroupItem<T>) -> Unit
|
onEvent: (RadioGroupItem<T>) -> Unit
|
||||||
@@ -22,13 +22,13 @@ fun <T> SpeedUnitRadioGroup(
|
|||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
items.forEach {
|
items.forEach {
|
||||||
SpeedUnitRadioButton(currentItem, it, onEvent)
|
SelectItemRadioButton(currentItem, it, onEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun <T> SpeedUnitRadioButton(
|
internal fun <T> SelectItemRadioButton(
|
||||||
selectedItem: T,
|
selectedItem: T,
|
||||||
displayedItem: RadioGroupItem<T>,
|
displayedItem: RadioGroupItem<T>,
|
||||||
onEvent: (RadioGroupItem<T>) -> Unit
|
onEvent: (RadioGroupItem<T>) -> Unit
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package no.nordicsemi.android.theme.view.dialog
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun String.toAnnotatedString() = buildAnnotatedString {
|
||||||
|
append(this@toAnnotatedString)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package no.nordicsemi.android.theme.view.dialog
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import no.nordicsemi.android.theme.NordicColors
|
||||||
|
import no.nordicsemi.android.theme.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StringListDialog(config: StringListDialogConfig) {
|
||||||
|
Dialog(onDismissRequest = { config.onResult(FlowCanceled) }) {
|
||||||
|
StringListView(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StringListView(config: StringListDialogConfig) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.height(300.dp),
|
||||||
|
backgroundColor = NordicColors.NordicGray4.value(),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
elevation = 0.dp
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString(),
|
||||||
|
fontSize = 20.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight(0.8f)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
|
||||||
|
config.items.forEachIndexed { i, entry ->
|
||||||
|
Column(modifier = Modifier.clickable { config.onResult(ItemSelectedResult(i)) }) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row {
|
||||||
|
config.leftIcon?.let {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
painter = painterResource(it),
|
||||||
|
contentDescription = "Content image",
|
||||||
|
colorFilter = ColorFilter.tint(
|
||||||
|
NordicColors.NordicDarkGray.value()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = entry,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i != config.items.size - 1) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { config.onResult(FlowCanceled) }) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.cancel),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package no.nordicsemi.android.theme.view.dialog
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
|
||||||
|
data class StringListDialogConfig(
|
||||||
|
val title: AnnotatedString? = null,
|
||||||
|
@DrawableRes
|
||||||
|
val leftIcon: Int? = null,
|
||||||
|
val items: List<String> = emptyList(),
|
||||||
|
val onResult: (StringListDialogResult) -> Unit
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package no.nordicsemi.android.theme.view.dialog
|
||||||
|
|
||||||
|
sealed class StringListDialogResult
|
||||||
|
|
||||||
|
data class ItemSelectedResult(val index: Int): StringListDialogResult()
|
||||||
|
|
||||||
|
object FlowCanceled : StringListDialogResult()
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">nRF Toolbox</string>
|
<string name="app_name">nRF Toolbox</string>
|
||||||
|
|
||||||
|
<string name="dialog">Dialog</string>
|
||||||
|
<string name="cancel">CANCEL</string>
|
||||||
|
|
||||||
<string name="close_app">Close the application.</string>
|
<string name="close_app">Close the application.</string>
|
||||||
<string name="back_screen">Close the current screen.</string>
|
<string name="back_screen">Close the current screen.</string>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package no.nordicsemi.android.csc.data
|
package no.nordicsemi.android.csc.data
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import no.nordicsemi.android.csc.view.CSCSettings
|
import no.nordicsemi.android.csc.view.CSCSettings
|
||||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||||
import no.nordicsemi.android.theme.view.RadioGroupItem
|
import no.nordicsemi.android.theme.view.RadioGroupItem
|
||||||
@@ -20,12 +19,6 @@ internal data class CSCData(
|
|||||||
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
|
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun drawItself() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private val speedWithUnit = when (selectedSpeedUnit) {
|
private val speedWithUnit = when (selectedSpeedUnit) {
|
||||||
SpeedUnit.M_S -> speed
|
SpeedUnit.M_S -> speed
|
||||||
SpeedUnit.KM_H -> speed * 3.6f
|
SpeedUnit.KM_H -> speed * 3.6f
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ internal class CSCDataHolder @Inject constructor() {
|
|||||||
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setHideWheelSizeDialog() {
|
||||||
|
_data.tryEmit(_data.value.copy(showDialog = false))
|
||||||
|
}
|
||||||
|
|
||||||
fun setDisplayWheelSizeDialog() {
|
fun setDisplayWheelSizeDialog() {
|
||||||
_data.tryEmit(_data.value.copy(showDialog = true))
|
_data.tryEmit(_data.value.copy(showDialog = true))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import no.nordicsemi.android.service.BatteryManager
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/** Cycling Speed and Cadence service UUID. */
|
/** Cycling Speed and Cadence service UUID. */
|
||||||
private val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
|
val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
/** Cycling Speed and Cadence Measurement characteristic UUID. */
|
/** Cycling Speed and Cadence Measurement characteristic UUID. */
|
||||||
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.R
|
||||||
import no.nordicsemi.android.csc.data.CSCData
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
import no.nordicsemi.android.theme.view.ScreenSection
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
import no.nordicsemi.android.theme.view.SelectItemRadioGroup
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
@@ -56,7 +56,7 @@ private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
SpeedUnitRadioGroup(state.selectedSpeedUnit, state.items()) {
|
SelectItemRadioGroup(state.selectedSpeedUnit, state.items()) {
|
||||||
onEvent(OnSelectedSpeedUnitSelected(it.unit))
|
onEvent(OnSelectedSpeedUnitSelected(it.unit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ internal object OnShowEditWheelSizeDialogButtonClick : CSCViewEvent()
|
|||||||
|
|
||||||
internal data class OnWheelSizeSelected(val wheelSize: Int, val wheelSizeDisplayInfo: String) : CSCViewEvent()
|
internal data class OnWheelSizeSelected(val wheelSize: Int, val wheelSizeDisplayInfo: String) : CSCViewEvent()
|
||||||
|
|
||||||
|
internal object OnCloseSelectWheelSizeDialog : CSCViewEvent()
|
||||||
|
|
||||||
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()
|
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()
|
||||||
|
|
||||||
internal object OnDisconnectButtonClick : CSCViewEvent()
|
internal object OnDisconnectButtonClick : CSCViewEvent()
|
||||||
|
|||||||
@@ -1,93 +1,47 @@
|
|||||||
package no.nordicsemi.android.csc.view
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.Card
|
|
||||||
import androidx.compose.material.TabRowDefaults.Divider
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.R
|
||||||
import no.nordicsemi.android.theme.NordicColors
|
|
||||||
import no.nordicsemi.android.theme.NordicColors.NordicLightGray
|
|
||||||
import no.nordicsemi.android.theme.TestTheme
|
import no.nordicsemi.android.theme.TestTheme
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.StringListDialog
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.StringListDialogConfig
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.StringListDialogResult
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.toAnnotatedString
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SelectWheelSizeDialog(onEvent: (OnWheelSizeSelected) -> Unit) {
|
internal fun SelectWheelSizeDialog(onEvent: (CSCViewEvent) -> Unit) {
|
||||||
Dialog(onDismissRequest = {}) {
|
|
||||||
SelectWheelSizeView(onEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SelectWheelSizeView(onEvent: (OnWheelSizeSelected) -> Unit) {
|
|
||||||
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
||||||
val wheelValues = stringArrayResource(R.array.wheel_values)
|
val wheelValues = stringArrayResource(R.array.wheel_values)
|
||||||
|
|
||||||
Card(
|
StringListDialog(createConfig(wheelEntries) {
|
||||||
modifier = Modifier.height(300.dp),
|
when (it) {
|
||||||
backgroundColor = NordicColors.NordicGray4.value(),
|
FlowCanceled -> onEvent(OnCloseSelectWheelSizeDialog)
|
||||||
shape = RoundedCornerShape(10.dp),
|
is ItemSelectedResult ->
|
||||||
elevation = 0.dp
|
onEvent(OnWheelSizeSelected(wheelValues[it.index].toInt(), wheelEntries[it.index]))
|
||||||
) {
|
}.exhaustive
|
||||||
Column {
|
})
|
||||||
Column(
|
}
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Wheel size",
|
|
||||||
fontSize = 28.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
@Composable
|
||||||
modifier = Modifier
|
private fun createConfig(entries: Array<String>, onResult: (StringListDialogResult) -> Unit): StringListDialogConfig {
|
||||||
.verticalScroll(rememberScrollState())
|
return StringListDialogConfig(
|
||||||
.padding(16.dp)
|
title = stringResource(id = R.string.csc_dialog_title).toAnnotatedString(),
|
||||||
) {
|
items = entries.toList(),
|
||||||
|
onResult = onResult
|
||||||
wheelEntries.forEachIndexed { i, entry ->
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = entry,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
onEvent(OnWheelSizeSelected(wheelValues[i].toInt(), entry))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (i != wheelEntries.size - 1) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Divider(color = NordicLightGray.value(), thickness = 1.dp/2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DefaultPreview() {
|
internal fun DefaultPreview() {
|
||||||
TestTheme {
|
TestTheme {
|
||||||
SelectWheelSizeView { }
|
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
||||||
|
StringListDialog(createConfig(wheelEntries) {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,18 +18,18 @@ import no.nordicsemi.android.theme.view.ScreenSection
|
|||||||
internal fun SensorsReadingView(state: CSCData) {
|
internal fun SensorsReadingView(state: CSCData) {
|
||||||
ScreenSection {
|
ScreenSection {
|
||||||
Column {
|
Column {
|
||||||
KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed())
|
KeyValueField(stringResource(id = R.string.csc_field_speed), state.displaySpeed())
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(stringResource(id = R.string.scs_field_cadence), state.displayCadence())
|
KeyValueField(stringResource(id = R.string.csc_field_cadence), state.displayCadence())
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(stringResource(id = R.string.scs_field_distance), state.displayDistance())
|
KeyValueField(stringResource(id = R.string.csc_field_distance), state.displayDistance())
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(
|
KeyValueField(
|
||||||
stringResource(id = R.string.scs_field_total_distance),
|
stringResource(id = R.string.csc_field_total_distance),
|
||||||
state.displayTotalDistance()
|
state.displayTotalDistance()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(stringResource(id = R.string.scs_field_gear_ratio), state.displayGearRatio())
|
KeyValueField(stringResource(id = R.string.csc_field_gear_ratio), state.displayGearRatio())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
value = state.wheelSizeDisplay,
|
value = state.wheelSizeDisplay,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
enabled = false,
|
enabled = false,
|
||||||
label = { Text(text = stringResource(id = R.string.scs_field_wheel_size)) },
|
label = { Text(text = stringResource(id = R.string.csc_field_wheel_size)) },
|
||||||
trailingIcon = { EditIcon(onEvent = onEvent) }
|
trailingIcon = { EditIcon(onEvent = onEvent) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package no.nordicsemi.android.csc.viewmodel
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import no.nordicsemi.android.csc.data.CSCDataHolder
|
import no.nordicsemi.android.csc.data.CSCDataHolder
|
||||||
import no.nordicsemi.android.csc.view.CSCViewEvent
|
import no.nordicsemi.android.csc.view.CSCViewEvent
|
||||||
|
import no.nordicsemi.android.csc.view.OnCloseSelectWheelSizeDialog
|
||||||
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
||||||
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
|
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
|
||||||
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
|
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
|
||||||
@@ -24,6 +25,7 @@ internal class CSCViewModel @Inject constructor(
|
|||||||
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
||||||
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
||||||
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
||||||
|
OnCloseSelectWheelSizeDialog -> onHideDialogEvent()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,4 +45,8 @@ internal class CSCViewModel @Inject constructor(
|
|||||||
finish()
|
finish()
|
||||||
dataHolder.clear()
|
dataHolder.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onHideDialogEvent() {
|
||||||
|
dataHolder.setHideWheelSizeDialog()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="csc_title">Cyclic and speed cadence</string>
|
<string name="csc_title">Cyclic and speed cadence</string>
|
||||||
|
|
||||||
<string name="scs_field_speed">Speed</string>
|
<string name="csc_dialog_title">Select wheel size</string>
|
||||||
<string name="scs_field_cadence">Cadence</string>
|
|
||||||
<string name="scs_field_distance">Distance</string>
|
|
||||||
<string name="scs_field_total_distance">Total Distance</string>
|
|
||||||
<string name="scs_field_gear_ratio">Gear Ratio</string>
|
|
||||||
|
|
||||||
<string name="scs_field_wheel_size">Wheel size</string>
|
<string name="csc_field_speed">Speed</string>
|
||||||
|
<string name="csc_field_cadence">Cadence</string>
|
||||||
|
<string name="csc_field_distance">Distance</string>
|
||||||
|
<string name="csc_field_total_distance">Total Distance</string>
|
||||||
|
<string name="csc_field_gear_ratio">Gear Ratio</string>
|
||||||
|
|
||||||
|
<string name="csc_field_wheel_size">Wheel size</string>
|
||||||
|
|
||||||
<string-array name="wheel_entries">
|
<string-array name="wheel_entries">
|
||||||
<item>60–622</item>
|
<item>60–622</item>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/** Glucose service UUID */
|
/** Glucose service UUID */
|
||||||
private val GLS_SERVICE_UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
|
val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
/** Glucose Measurement characteristic UUID */
|
/** Glucose Measurement characteristic UUID */
|
||||||
private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
|
private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
|||||||
import no.nordicsemi.android.gls.viewmodel.OnWorkingModeSelected
|
import no.nordicsemi.android.gls.viewmodel.OnWorkingModeSelected
|
||||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||||
import no.nordicsemi.android.theme.view.ScreenSection
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
import no.nordicsemi.android.theme.view.SelectItemRadioGroup
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||||
@@ -55,7 +55,7 @@ internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Uni
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||||
ScreenSection {
|
ScreenSection {
|
||||||
SpeedUnitRadioGroup(state.selectedMode, state.modeItems()) {
|
SelectItemRadioGroup(state.selectedMode, state.modeItems()) {
|
||||||
onEvent(OnWorkingModeSelected(it.unit))
|
onEvent(OnWorkingModeSelected(it.unit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import no.nordicsemi.android.log.LogContract
|
|||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
private val HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
|
val HR_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
|
||||||
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb")
|
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb")
|
||||||
private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
|
private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import no.nordicsemi.android.log.LogContract
|
|||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
private val HT_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb")
|
val HT_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb")
|
||||||
private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb")
|
private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import no.nordicsemi.android.hts.data.HTSData
|
|||||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||||
import no.nordicsemi.android.theme.view.KeyValueField
|
import no.nordicsemi.android.theme.view.KeyValueField
|
||||||
import no.nordicsemi.android.theme.view.ScreenSection
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
import no.nordicsemi.android.theme.view.SelectItemRadioGroup
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
|
internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
|
||||||
@@ -33,7 +33,7 @@ internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Uni
|
|||||||
|
|
||||||
ScreenSection {
|
ScreenSection {
|
||||||
Box(modifier = Modifier.padding(16.dp)) {
|
Box(modifier = Modifier.padding(16.dp)) {
|
||||||
SpeedUnitRadioGroup(state.temperatureUnit, state.temperatureSettingsItems()) {
|
SelectItemRadioGroup(state.temperatureUnit, state.temperatureSettingsItems()) {
|
||||||
onEvent(OnTemperatureUnitSelected(it.unit))
|
onEvent(OnTemperatureUnitSelected(it.unit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
profile_permission/build.gradle
Normal file
15
profile_permission/build.gradle
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apply from: rootProject.file("library.gradle")
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":lib_utils")
|
||||||
|
implementation project(":lib_theme")
|
||||||
|
implementation project(":lib_service")
|
||||||
|
|
||||||
|
implementation libs.material
|
||||||
|
implementation libs.google.permissions
|
||||||
|
|
||||||
|
implementation libs.bundles.compose
|
||||||
|
implementation libs.compose.lifecycle
|
||||||
|
implementation libs.compose.activity
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner
|
package no.nordicsemi.android.permission
|
||||||
|
|
||||||
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
|
||||||
14
profile_permission/src/main/AndroidManifest.xml
Normal file
14
profile_permission/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="no.nordicsemi.android.permission">
|
||||||
|
|
||||||
|
<uses-feature android:name="android.software.companion_device_setup"/>
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner
|
package no.nordicsemi.android.permission
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -7,7 +7,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import no.nordicsemi.android.scanner.tools.PermissionHelper
|
import no.nordicsemi.android.permission.tools.PermissionHelper
|
||||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -22,14 +22,8 @@ internal object HiltModule {
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun createSelectedBluetoothDeviceHolder(
|
fun createSelectedBluetoothDeviceHolder(): SelectedBluetoothDeviceHolder {
|
||||||
@ApplicationContext context: Context,
|
return SelectedBluetoothDeviceHolder()
|
||||||
bluetoothAdapter: BluetoothAdapter?
|
|
||||||
): SelectedBluetoothDeviceHolder {
|
|
||||||
return SelectedBluetoothDeviceHolder(
|
|
||||||
context,
|
|
||||||
bluetoothAdapter
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.tools
|
package no.nordicsemi.android.permission.tools
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.tools
|
package no.nordicsemi.android.permission.tools
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.tools
|
package no.nordicsemi.android.permission.tools
|
||||||
|
|
||||||
enum class ScannerStatus {
|
enum class ScannerStatus {
|
||||||
ENABLED, DISABLED, NOT_AVAILABLE
|
ENABLED, DISABLED, NOT_AVAILABLE
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.view
|
package no.nordicsemi.android.permission.view
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.scanner.R
|
import no.nordicsemi.android.permission.R
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.view
|
package no.nordicsemi.android.permission.view
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.scanner.R
|
import no.nordicsemi.android.permission.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotConnectedScreen(
|
private fun NotConnectedScreen(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.view
|
package no.nordicsemi.android.permission.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -29,7 +29,7 @@ import androidx.core.content.ContextCompat.startActivity
|
|||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.PermissionsRequired
|
import com.google.accompanist.permissions.PermissionsRequired
|
||||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||||
import no.nordicsemi.android.scanner.R
|
import no.nordicsemi.android.permission.R
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner.viewmodel
|
package no.nordicsemi.android.permission.viewmodel
|
||||||
|
|
||||||
enum class BluetoothPermissionState {
|
enum class BluetoothPermissionState {
|
||||||
PERMISSION_REQUIRED,
|
PERMISSION_REQUIRED,
|
||||||
26
profile_permission/src/main/res/values/strings.xml
Normal file
26
profile_permission/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="scanner__devices_list">BLE devices</string>
|
||||||
|
|
||||||
|
<string name="scanner__permission_rationale">The location permission is required when using Bluetooth LE, because surrounding devices can expose user\'s location. Please grant the permission.</string>
|
||||||
|
<string name="scanner__permission_denied">Location permission denied. Please, grant us access on the Settings screen.</string>
|
||||||
|
<string name="scanner__open_settings">Open settings</string>
|
||||||
|
<string name="scanner__feature_not_available">Feature not available</string>
|
||||||
|
|
||||||
|
<string name="scanner__list_of_devices">List of devices</string>
|
||||||
|
<string name="scanner__error">Scanning failed due to technical reason.</string>
|
||||||
|
<string name="scanner__no_name">Name: NONE</string>
|
||||||
|
|
||||||
|
<string name="csc_no_connection">No device connected</string>
|
||||||
|
<string name="csc_connect">Connect</string>
|
||||||
|
|
||||||
|
<string name="scanner__button_ok">Grant</string>
|
||||||
|
<string name="scanner__button_nope">Deny</string>
|
||||||
|
|
||||||
|
<string name="scanner__request_permission">Request permission</string>
|
||||||
|
|
||||||
|
<string name="scanner__bluetooth_not_available">Bluetooth not available.</string>
|
||||||
|
<string name="scanner__bluetooth_not_enabled">Bluetooth not enabled.</string>
|
||||||
|
<string name="scanner__bluetooth_open_settings_info">To enable Bluetooth please open settings.</string>
|
||||||
|
<string name="scanner__bluetooth_open_settings">Open settings</string>
|
||||||
|
</resources>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.scanner
|
package no.nordicsemi.android.permission
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ dependencies {
|
|||||||
implementation project(":lib_theme")
|
implementation project(":lib_theme")
|
||||||
implementation project(":lib_service")
|
implementation project(":lib_service")
|
||||||
|
|
||||||
|
implementation libs.scanner
|
||||||
implementation libs.material
|
implementation libs.material
|
||||||
implementation libs.google.permissions
|
implementation libs.google.permissions
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package no.nordicsemi.android.permission
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("no.nordicsemi.android.scanner.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
<?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.scanner">
|
package="no.nordicsemi.android.scanner" >
|
||||||
|
|
||||||
<uses-feature android:name="android.software.companion_device_setup"/>
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package no.nordicsemi.android.scanner.data
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
|
||||||
|
data class ScanDevicesData(
|
||||||
|
val devices: List<BluetoothDevice> = emptyList()
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun copyWithNewDevice(device: BluetoothDevice): ScanDevicesData {
|
||||||
|
if (devices.contains(device)) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
val newDevices = devices + device
|
||||||
|
return copy(devices = newDevices)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyWithNewDevices(bleDevices: List<BluetoothDevice>): ScanDevicesData {
|
||||||
|
val filteredDevice = bleDevices.filter { !devices.contains(it) }
|
||||||
|
val newDevices = devices + filteredDevice
|
||||||
|
return copy(devices = newDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +1,72 @@
|
|||||||
package no.nordicsemi.android.scanner.view
|
package no.nordicsemi.android.scanner.view
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.companion.AssociationRequest
|
|
||||||
import android.companion.BluetoothLeDeviceFilter
|
|
||||||
import android.companion.CompanionDeviceManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.IntentSender
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.IntentSenderRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import no.nordicsemi.android.scanner.R
|
||||||
|
import no.nordicsemi.android.scanner.viewmodel.ScanDevicesViewModel
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.StringListDialog
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.StringListDialogConfig
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.StringListDialogResult
|
||||||
|
import no.nordicsemi.android.theme.view.dialog.StringListView
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanDeviceScreen(finish: (ScanDeviceScreenResult) -> Unit) {
|
fun ScanDeviceScreen(serviceId: String, finishAction: (ScanDeviceScreenResult) -> Unit) {
|
||||||
val deviceManager =
|
val viewModel: ScanDevicesViewModel = hiltViewModel()
|
||||||
LocalContext.current.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
val data = viewModel.data.collectAsState().value
|
||||||
|
|
||||||
val contract = ActivityResultContracts.StartIntentSenderForResult()
|
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||||
val launcher = rememberLauncherForActivityResult(contract = contract) {
|
|
||||||
val result = if (it.resultCode == Activity.RESULT_OK) {
|
LaunchedEffect(isScreenActive) {
|
||||||
ScanDeviceScreenResult.SUCCESS
|
if (!isScreenActive) {
|
||||||
|
viewModel.stopScanner()
|
||||||
|
finishAction(ScanDeviceScreenResult.OK)
|
||||||
} else {
|
} else {
|
||||||
ScanDeviceScreenResult.CANCEL
|
viewModel.startScan(serviceId)
|
||||||
}
|
}
|
||||||
finish(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasBeenInvoked = remember { mutableStateOf(false) }
|
val names = data.devices.map { it.displayName() }
|
||||||
if (hasBeenInvoked.value) {
|
StringListDialog(createConfig(names) {
|
||||||
return
|
when (it) {
|
||||||
|
FlowCanceled -> finishAction(ScanDeviceScreenResult.CANCEL)
|
||||||
|
is ItemSelectedResult -> viewModel.onEvent(OnDeviceSelected(data.devices[it.index]))
|
||||||
|
}.exhaustive
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun createConfig(devices: List<String>, onClick: (StringListDialogResult) -> Unit): StringListDialogConfig {
|
||||||
|
val annotatedString = buildAnnotatedString {
|
||||||
|
append(stringResource(id = R.string.connect_to))
|
||||||
|
append(" ")
|
||||||
|
withStyle(style = SpanStyle(fontWeight = FontWeight.W800)) {
|
||||||
|
append(stringResource(id = R.string.app_name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StringListDialogConfig(
|
||||||
|
title = annotatedString,
|
||||||
|
leftIcon = R.drawable.ic_bluetooth,
|
||||||
|
items = devices.map { it }
|
||||||
|
) {
|
||||||
|
onClick(it)
|
||||||
}
|
}
|
||||||
hasBeenInvoked.value = true
|
|
||||||
|
|
||||||
val deviceFilter = BluetoothLeDeviceFilter.Builder()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
|
|
||||||
.addDeviceFilter(deviceFilter)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
deviceManager.associate(pairingRequest,
|
|
||||||
object : CompanionDeviceManager.Callback() {
|
|
||||||
override fun onDeviceFound(chooserLauncher: IntentSender) {
|
|
||||||
val request = IntentSenderRequest.Builder(chooserLauncher).build()
|
|
||||||
launcher.launch(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(error: CharSequence?) {
|
|
||||||
}
|
|
||||||
}, null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ScanDeviceScreenResult {
|
@Preview
|
||||||
SUCCESS, CANCEL
|
@Composable
|
||||||
|
fun ScanDeviceScreenPreview() {
|
||||||
|
val items = listOf("Nordic_HRS", "iPods PRO")
|
||||||
|
val config = createConfig(items) {}
|
||||||
|
StringListView(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package no.nordicsemi.android.scanner.view
|
||||||
|
|
||||||
|
enum class ScanDeviceScreenResult {
|
||||||
|
OK, CANCEL
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package no.nordicsemi.android.scanner.view
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
|
||||||
|
sealed class ScanDevicesViewEvent
|
||||||
|
|
||||||
|
data class OnDeviceSelected(val device: BluetoothDevice) : ScanDevicesViewEvent()
|
||||||
|
|
||||||
|
object OnCancelButtonClick : ScanDevicesViewEvent()
|
||||||
|
|
||||||
|
fun BluetoothDevice.displayName(): String {
|
||||||
|
return name ?: address
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package no.nordicsemi.android.scanner.viewmodel
|
||||||
|
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import no.nordicsemi.android.scanner.data.ScanDevicesData
|
||||||
|
import no.nordicsemi.android.scanner.view.OnCancelButtonClick
|
||||||
|
import no.nordicsemi.android.scanner.view.OnDeviceSelected
|
||||||
|
import no.nordicsemi.android.scanner.view.ScanDevicesViewEvent
|
||||||
|
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||||
|
import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat
|
||||||
|
import no.nordicsemi.android.support.v18.scanner.ScanCallback
|
||||||
|
import no.nordicsemi.android.support.v18.scanner.ScanFilter
|
||||||
|
import no.nordicsemi.android.support.v18.scanner.ScanResult
|
||||||
|
import no.nordicsemi.android.support.v18.scanner.ScanSettings
|
||||||
|
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ScanDevicesViewModel @Inject constructor(
|
||||||
|
private val deviceHolder: SelectedBluetoothDeviceHolder
|
||||||
|
) : CloseableViewModel() {
|
||||||
|
|
||||||
|
val data = MutableStateFlow(ScanDevicesData())
|
||||||
|
|
||||||
|
private val scanner = BluetoothLeScannerCompat.getScanner()
|
||||||
|
|
||||||
|
private val scanCallback = object : ScanCallback() {
|
||||||
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
|
data.tryEmit(data.value.copyWithNewDevice(result.device))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||||
|
val devices = results.map { it.device }
|
||||||
|
data.tryEmit(data.value.copyWithNewDevices(devices))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanFailed(errorCode: Int) {
|
||||||
|
//todo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEvent(event: ScanDevicesViewEvent) {
|
||||||
|
when (event) {
|
||||||
|
OnCancelButtonClick -> finish()
|
||||||
|
is OnDeviceSelected -> onDeviceSelected(event)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDeviceSelected(event: OnDeviceSelected) {
|
||||||
|
deviceHolder.attachDevice(event.device)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startScan(serviceId: String) {
|
||||||
|
val scanner: BluetoothLeScannerCompat = BluetoothLeScannerCompat.getScanner()
|
||||||
|
val settings: ScanSettings = ScanSettings.Builder()
|
||||||
|
.setLegacy(false)
|
||||||
|
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||||
|
.setReportDelay(5000)
|
||||||
|
.setUseHardwareBatchingIfSupported(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
val filters: MutableList<ScanFilter> = ArrayList()
|
||||||
|
val uuid = ParcelUuid.fromString(serviceId)
|
||||||
|
filters.add(ScanFilter.Builder().setServiceUuid(uuid).build())
|
||||||
|
|
||||||
|
scanner.startScan(filters, settings, scanCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopScanner() {
|
||||||
|
scanner.stopScan(scanCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
profile_scanner/src/main/res/drawable/ic_bluetooth.xml
Normal file
10
profile_scanner/src/main/res/drawable/ic_bluetooth.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M17.71,7.71L12,2h-1v7.59L6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 11,14.41L11,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,5.83l1.88,1.88L13,9.59L13,5.83zM14.88,16.29L13,18.17v-3.76l1.88,1.88z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,26 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="scanner__devices_list">BLE devices</string>
|
<string name="connect_to">Link with</string>
|
||||||
|
|
||||||
<string name="scanner__permission_rationale">The location permission is required when using Bluetooth LE, because surrounding devices can expose user\'s location. Please grant the permission.</string>
|
|
||||||
<string name="scanner__permission_denied">Location permission denied. Please, grant us access on the Settings screen.</string>
|
|
||||||
<string name="scanner__open_settings">Open settings</string>
|
|
||||||
<string name="scanner__feature_not_available">Feature not available</string>
|
|
||||||
|
|
||||||
<string name="scanner__list_of_devices">List of devices</string>
|
|
||||||
<string name="scanner__error">Scanning failed due to technical reason.</string>
|
|
||||||
<string name="scanner__no_name">Name: NONE</string>
|
|
||||||
|
|
||||||
<string name="csc_no_connection">No device connected</string>
|
|
||||||
<string name="csc_connect">Connect</string>
|
|
||||||
|
|
||||||
<string name="scanner__button_ok">Grant</string>
|
|
||||||
<string name="scanner__button_nope">Deny</string>
|
|
||||||
|
|
||||||
<string name="scanner__request_permission">Request permission</string>
|
|
||||||
|
|
||||||
<string name="scanner__bluetooth_not_available">Bluetooth not available.</string>
|
|
||||||
<string name="scanner__bluetooth_not_enabled">Bluetooth not enabled.</string>
|
|
||||||
<string name="scanner__bluetooth_open_settings_info">To enable Bluetooth please open settings.</string>
|
|
||||||
<string name="scanner__bluetooth_open_settings">Open settings</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package no.nordicsemi.android.permission
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ dependencyResolutionManagement {
|
|||||||
alias('kotlin-coroutines').to('org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2')
|
alias('kotlin-coroutines').to('org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2')
|
||||||
alias('google-permissions').to('com.google.accompanist:accompanist-permissions:0.18.0')
|
alias('google-permissions').to('com.google.accompanist:accompanist-permissions:0.18.0')
|
||||||
alias('chart').to('com.github.PhilJay:MPAndroidChart:v3.1.0')
|
alias('chart').to('com.github.PhilJay:MPAndroidChart:v3.1.0')
|
||||||
|
alias('scanner').to('no.nordicsemi.android.support.v18:scanner:1.6.0')
|
||||||
|
|
||||||
//-- Test ------------------------------------------------------------------------------
|
//-- Test ------------------------------------------------------------------------------
|
||||||
alias('test-junit').to('junit:junit:4.13.2')
|
alias('test-junit').to('junit:junit:4.13.2')
|
||||||
@@ -65,7 +66,7 @@ include ':profile_csc'
|
|||||||
include ':profile_gls'
|
include ':profile_gls'
|
||||||
include ':profile_hrs'
|
include ':profile_hrs'
|
||||||
include ':profile_hts'
|
include ':profile_hts'
|
||||||
include ':profile_scanner'
|
include ':profile_permission'
|
||||||
|
|
||||||
include ':lib_service'
|
include ':lib_service'
|
||||||
include ':lib_theme'
|
include ':lib_theme'
|
||||||
@@ -78,3 +79,4 @@ if (file('../Android-BLE-Library').exists()) {
|
|||||||
if (file('../Android-Scanner-Compat-Library').exists()) {
|
if (file('../Android-Scanner-Compat-Library').exists()) {
|
||||||
includeBuild('../Android-Scanner-Compat-Library')
|
includeBuild('../Android-Scanner-Compat-Library')
|
||||||
}
|
}
|
||||||
|
include ':profile_scanner'
|
||||||
|
|||||||
Reference in New Issue
Block a user