Replace CompanionDeviceManager with Scanner library.

This commit is contained in:
Sylwester Zieliński
2021-10-12 14:39:23 +02:00
parent 90fa2db2ad
commit 5a84cc495b
52 changed files with 569 additions and 235 deletions

View File

@@ -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")

View File

@@ -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)
} }
} }

View File

@@ -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}");
} }

View File

@@ -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
}

View File

@@ -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")
}
} }
} }

View File

@@ -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 forgetDevice() { fun attachDevice(newDevice: BluetoothDevice) {
device?.let { device = newDevice
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
deviceManager.disassociate(it.address)
} }
fun forgetDevice() {
device = null
} }
} }

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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),
)
}
}
}
}
}

View File

@@ -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
)

View File

@@ -0,0 +1,7 @@
package no.nordicsemi.android.theme.view.dialog
sealed class StringListDialogResult
data class ItemSelectedResult(val index: Int): StringListDialogResult()
object FlowCanceled : StringListDialogResult()

View File

@@ -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>

View File

@@ -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

View File

@@ -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))
} }

View File

@@ -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")

View File

@@ -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))
} }
} }

View File

@@ -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()

View File

@@ -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 @Composable
) { private fun createConfig(entries: Array<String>, onResult: (StringListDialogResult) -> Unit): StringListDialogConfig {
Text( return StringListDialogConfig(
text = "Wheel size", title = stringResource(id = R.string.csc_dialog_title).toAnnotatedString(),
fontSize = 28.sp, items = entries.toList(),
fontWeight = FontWeight.Bold onResult = onResult
) )
}
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
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) {})
} }
} }

View File

@@ -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())
} }
} }

View File

@@ -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) }
) )
} }

View File

@@ -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()
}
} }

View File

@@ -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&#8211;622</item> <item>60&#8211;622</item>

View File

@@ -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")

View File

@@ -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))
} }
} }

View File

@@ -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")

View File

@@ -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")
/** /**

View File

@@ -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))
} }
} }

View 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
}

View File

@@ -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

View 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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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)

View File

@@ -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,

View 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>

View File

@@ -1,4 +1,4 @@
package no.nordicsemi.android.scanner package no.nordicsemi.android.permission
import org.junit.Test import org.junit.Test

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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)
hasBeenInvoked.value = true is ItemSelectedResult -> viewModel.onEvent(OnDeviceSelected(data.devices[it.index]))
}.exhaustive
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 { @Composable
SUCCESS, CANCEL 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)
}
}
@Preview
@Composable
fun ScanDeviceScreenPreview() {
val items = listOf("Nordic_HRS", "iPods PRO")
val config = createConfig(items) {}
StringListView(config)
} }

View File

@@ -0,0 +1,5 @@
package no.nordicsemi.android.scanner.view
enum class ScanDeviceScreenResult {
OK, CANCEL
}

View File

@@ -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
}

View File

@@ -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)
}
}

View 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>

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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'