mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 07:24:22 +01:00
Replace CompanionDeviceManager with Scanner library.
This commit is contained in:
@@ -54,6 +54,7 @@ dependencies {
|
||||
implementation project(':profile_hrs')
|
||||
implementation project(':profile_hts')
|
||||
implementation project(':profile_gls')
|
||||
implementation project(':profile_permission')
|
||||
implementation project(':profile_scanner')
|
||||
implementation project(":lib_theme")
|
||||
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.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import no.nordicsemi.android.csc.view.CSCScreen
|
||||
import no.nordicsemi.android.gls.view.GLSScreen
|
||||
import no.nordicsemi.android.hrs.view.HRSScreen
|
||||
import no.nordicsemi.android.hts.view.HTSScreen
|
||||
import no.nordicsemi.android.scanner.view.BluetoothNotAvailableScreen
|
||||
import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen
|
||||
import no.nordicsemi.android.scanner.view.RequestPermissionScreen
|
||||
import no.nordicsemi.android.permission.view.BluetoothNotAvailableScreen
|
||||
import no.nordicsemi.android.permission.view.BluetoothNotEnabledScreen
|
||||
import no.nordicsemi.android.permission.view.RequestPermissionScreen
|
||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreen
|
||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
||||
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
||||
@@ -56,10 +58,13 @@ internal fun HomeScreen() {
|
||||
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
||||
BluetoothNotEnabledScreen(continueAction)
|
||||
}
|
||||
composable(NavDestination.DEVICE_NOT_CONNECTED.id) {
|
||||
ScanDeviceScreen {
|
||||
composable(
|
||||
NavDestination.DEVICE_NOT_CONNECTED.id,
|
||||
arguments = listOf(navArgument("args") { type = NavType.StringType })
|
||||
) {
|
||||
ScanDeviceScreen(it.arguments?.getString(ARGS_KEY)!!) {
|
||||
when (it) {
|
||||
ScanDeviceScreenResult.SUCCESS -> viewModel.finish()
|
||||
ScanDeviceScreenResult.OK -> viewModel.finish()
|
||||
ScanDeviceScreenResult.CANCEL -> viewModel.navigateUp()
|
||||
}.exhaustive
|
||||
}
|
||||
@@ -67,7 +72,7 @@ internal fun HomeScreen() {
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
navController.navigate(state.id)
|
||||
navController.navigate(state.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
const val ARGS_KEY = "args"
|
||||
|
||||
enum class NavDestination(val id: String) {
|
||||
HOME("home-screen"),
|
||||
CSC("csc-screen"),
|
||||
@@ -9,5 +11,5 @@ enum class NavDestination(val id: String) {
|
||||
REQUEST_PERMISSION("request-permission"),
|
||||
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import no.nordicsemi.android.scanner.tools.NordicBleScanner
|
||||
import no.nordicsemi.android.scanner.tools.PermissionHelper
|
||||
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
||||
import no.nordicsemi.android.csc.service.CYCLING_SPEED_AND_CADENCE_SERVICE_UUID
|
||||
import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID
|
||||
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.scanner.viewmodel.BluetoothPermissionState
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -17,7 +21,7 @@ class NavigationViewModel @Inject constructor(
|
||||
private val selectedDevice: SelectedBluetoothDeviceHolder
|
||||
): ViewModel() {
|
||||
|
||||
val state= MutableStateFlow(NavDestination.HOME)
|
||||
val state= MutableStateFlow(NavigationTarget(NavDestination.HOME))
|
||||
private var targetDestination = NavDestination.HOME
|
||||
|
||||
fun navigate(destination: NavDestination) {
|
||||
@@ -27,11 +31,11 @@ class NavigationViewModel @Inject constructor(
|
||||
|
||||
fun navigateUp() {
|
||||
targetDestination = NavDestination.HOME
|
||||
state.value = NavDestination.HOME
|
||||
state.value = NavigationTarget(NavDestination.HOME)
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
if (state.value != targetDestination) {
|
||||
if (state.value.destination != targetDestination) {
|
||||
navigateToNextScreen()
|
||||
}
|
||||
}
|
||||
@@ -47,12 +51,33 @@ class NavigationViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun navigateToNextScreen() {
|
||||
state.value = when (getBluetoothState()) {
|
||||
val destination = when (getBluetoothState()) {
|
||||
BluetoothPermissionState.PERMISSION_REQUIRED -> NavDestination.REQUEST_PERMISSION
|
||||
BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE -> NavDestination.BLUETOOTH_NOT_AVAILABLE
|
||||
BluetoothPermissionState.BLUETOOTH_NOT_ENABLED -> NavDestination.BLUETOOTH_NOT_ENABLED
|
||||
BluetoothPermissionState.DEVICE_NOT_CONNECTED -> NavDestination.DEVICE_NOT_CONNECTED
|
||||
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
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.Context
|
||||
|
||||
class SelectedBluetoothDeviceHolder constructor(
|
||||
private val context: Context,
|
||||
private val bluetoothAdapter: BluetoothAdapter?
|
||||
) {
|
||||
class SelectedBluetoothDeviceHolder {
|
||||
|
||||
val device: BluetoothDevice?
|
||||
get() {
|
||||
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
|
||||
}
|
||||
var device: BluetoothDevice? = null
|
||||
private set
|
||||
|
||||
fun isBondingRequired(): Boolean {
|
||||
return device?.bondState == BluetoothDevice.BOND_NONE
|
||||
@@ -23,10 +14,11 @@ class SelectedBluetoothDeviceHolder constructor(
|
||||
device?.createBond()
|
||||
}
|
||||
|
||||
fun attachDevice(newDevice: BluetoothDevice) {
|
||||
device = newDevice
|
||||
}
|
||||
|
||||
fun forgetDevice() {
|
||||
device?.let {
|
||||
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||
deviceManager.disassociate(it.address)
|
||||
}
|
||||
device = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun <T> SpeedUnitRadioGroup(
|
||||
fun <T> SelectItemRadioGroup(
|
||||
currentItem: T,
|
||||
items: List<RadioGroupItem<T>>,
|
||||
onEvent: (RadioGroupItem<T>) -> Unit
|
||||
@@ -22,13 +22,13 @@ fun <T> SpeedUnitRadioGroup(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
items.forEach {
|
||||
SpeedUnitRadioButton(currentItem, it, onEvent)
|
||||
SelectItemRadioButton(currentItem, it, onEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun <T> SpeedUnitRadioButton(
|
||||
internal fun <T> SelectItemRadioButton(
|
||||
selectedItem: T,
|
||||
displayedItem: RadioGroupItem<T>,
|
||||
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>
|
||||
<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="back_screen">Close the current screen.</string>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package no.nordicsemi.android.csc.data
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import no.nordicsemi.android.csc.view.CSCSettings
|
||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||
import no.nordicsemi.android.theme.view.RadioGroupItem
|
||||
@@ -20,12 +19,6 @@ internal data class CSCData(
|
||||
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
|
||||
) {
|
||||
|
||||
@Composable
|
||||
fun drawItself() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private val speedWithUnit = when (selectedSpeedUnit) {
|
||||
SpeedUnit.M_S -> speed
|
||||
SpeedUnit.KM_H -> speed * 3.6f
|
||||
|
||||
@@ -24,6 +24,10 @@ internal class CSCDataHolder @Inject constructor() {
|
||||
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
||||
}
|
||||
|
||||
fun setHideWheelSizeDialog() {
|
||||
_data.tryEmit(_data.value.copy(showDialog = false))
|
||||
}
|
||||
|
||||
fun setDisplayWheelSizeDialog() {
|
||||
_data.tryEmit(_data.value.copy(showDialog = true))
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import no.nordicsemi.android.service.BatteryManager
|
||||
import java.util.*
|
||||
|
||||
/** 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. */
|
||||
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.data.CSCData
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
||||
import no.nordicsemi.android.theme.view.SelectItemRadioGroup
|
||||
|
||||
@Composable
|
||||
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))
|
||||
|
||||
SpeedUnitRadioGroup(state.selectedSpeedUnit, state.items()) {
|
||||
SelectItemRadioGroup(state.selectedSpeedUnit, state.items()) {
|
||||
onEvent(OnSelectedSpeedUnitSelected(it.unit))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ internal object OnShowEditWheelSizeDialogButtonClick : 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 object OnDisconnectButtonClick : CSCViewEvent()
|
||||
|
||||
@@ -1,93 +1,47 @@
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
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.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
|
||||
internal fun SelectWheelSizeDialog(onEvent: (OnWheelSizeSelected) -> Unit) {
|
||||
Dialog(onDismissRequest = {}) {
|
||||
SelectWheelSizeView(onEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectWheelSizeView(onEvent: (OnWheelSizeSelected) -> Unit) {
|
||||
internal fun SelectWheelSizeDialog(onEvent: (CSCViewEvent) -> Unit) {
|
||||
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
||||
val wheelValues = stringArrayResource(R.array.wheel_values)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.height(300.dp),
|
||||
backgroundColor = NordicColors.NordicGray4.value(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
elevation = 0.dp
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Wheel size",
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
StringListDialog(createConfig(wheelEntries) {
|
||||
when (it) {
|
||||
FlowCanceled -> onEvent(OnCloseSelectWheelSizeDialog)
|
||||
is ItemSelectedResult ->
|
||||
onEvent(OnWheelSizeSelected(wheelValues[it.index].toInt(), wheelEntries[it.index]))
|
||||
}.exhaustive
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
private fun createConfig(entries: Array<String>, onResult: (StringListDialogResult) -> Unit): StringListDialogConfig {
|
||||
return StringListDialogConfig(
|
||||
title = stringResource(id = R.string.csc_dialog_title).toAnnotatedString(),
|
||||
items = entries.toList(),
|
||||
onResult = onResult
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun DefaultPreview() {
|
||||
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) {
|
||||
ScreenSection {
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
KeyValueField(
|
||||
stringResource(id = R.string.scs_field_total_distance),
|
||||
stringResource(id = R.string.csc_field_total_distance),
|
||||
state.displayTotalDistance()
|
||||
)
|
||||
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,
|
||||
onValueChange = { },
|
||||
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) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package no.nordicsemi.android.csc.viewmodel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import no.nordicsemi.android.csc.data.CSCDataHolder
|
||||
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.OnSelectedSpeedUnitSelected
|
||||
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
|
||||
@@ -24,6 +25,7 @@ internal class CSCViewModel @Inject constructor(
|
||||
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
||||
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
||||
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
||||
OnCloseSelectWheelSizeDialog -> onHideDialogEvent()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@@ -43,4 +45,8 @@ internal class CSCViewModel @Inject constructor(
|
||||
finish()
|
||||
dataHolder.clear()
|
||||
}
|
||||
|
||||
private fun onHideDialogEvent() {
|
||||
dataHolder.setHideWheelSizeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
<resources>
|
||||
<string name="csc_title">Cyclic and speed cadence</string>
|
||||
|
||||
<string name="scs_field_speed">Speed</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="csc_dialog_title">Select wheel size</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">
|
||||
<item>60–622</item>
|
||||
|
||||
@@ -59,7 +59,7 @@ import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** 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 */
|
||||
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.theme.view.BatteryLevelView
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
||||
import no.nordicsemi.android.theme.view.SelectItemRadioGroup
|
||||
|
||||
@Composable
|
||||
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
@@ -55,7 +55,7 @@ internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Uni
|
||||
@Composable
|
||||
private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
ScreenSection {
|
||||
SpeedUnitRadioGroup(state.selectedMode, state.modeItems()) {
|
||||
SelectItemRadioGroup(state.selectedMode, state.modeItems()) {
|
||||
onEvent(OnWorkingModeSelected(it.unit))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import no.nordicsemi.android.log.LogContract
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
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 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 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")
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ import no.nordicsemi.android.hts.data.HTSData
|
||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||
import no.nordicsemi.android.theme.view.KeyValueField
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
||||
import no.nordicsemi.android.theme.view.SelectItemRadioGroup
|
||||
|
||||
@Composable
|
||||
internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
|
||||
@@ -33,7 +33,7 @@ internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Uni
|
||||
|
||||
ScreenSection {
|
||||
Box(modifier = Modifier.padding(16.dp)) {
|
||||
SpeedUnitRadioGroup(state.temperatureUnit, state.temperatureSettingsItems()) {
|
||||
SelectItemRadioGroup(state.temperatureUnit, state.temperatureSettingsItems()) {
|
||||
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.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.content.Context
|
||||
@@ -7,7 +7,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
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 javax.inject.Singleton
|
||||
|
||||
@@ -22,14 +22,8 @@ internal object HiltModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun createSelectedBluetoothDeviceHolder(
|
||||
@ApplicationContext context: Context,
|
||||
bluetoothAdapter: BluetoothAdapter?
|
||||
): SelectedBluetoothDeviceHolder {
|
||||
return SelectedBluetoothDeviceHolder(
|
||||
context,
|
||||
bluetoothAdapter
|
||||
)
|
||||
fun createSelectedBluetoothDeviceHolder(): SelectedBluetoothDeviceHolder {
|
||||
return SelectedBluetoothDeviceHolder()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@@ -1,4 +1,4 @@
|
||||
package no.nordicsemi.android.scanner.tools
|
||||
package no.nordicsemi.android.permission.tools
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.content.Context
|
||||
@@ -1,4 +1,4 @@
|
||||
package no.nordicsemi.android.scanner.tools
|
||||
package no.nordicsemi.android.permission.tools
|
||||
|
||||
enum class ScannerStatus {
|
||||
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.bluetooth.BluetoothAdapter
|
||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.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.Column
|
||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.scanner.R
|
||||
import no.nordicsemi.android.permission.R
|
||||
|
||||
@Composable
|
||||
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.Intent
|
||||
@@ -29,7 +29,7 @@ import androidx.core.content.ContextCompat.startActivity
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionsRequired
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@@ -1,4 +1,4 @@
|
||||
package no.nordicsemi.android.scanner.viewmodel
|
||||
package no.nordicsemi.android.permission.viewmodel
|
||||
|
||||
enum class BluetoothPermissionState {
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ dependencies {
|
||||
implementation project(":lib_theme")
|
||||
implementation project(":lib_service")
|
||||
|
||||
implementation libs.scanner
|
||||
implementation libs.material
|
||||
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"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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"/>
|
||||
package="no.nordicsemi.android.scanner" >
|
||||
|
||||
<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>
|
||||
@@ -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
|
||||
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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
|
||||
fun ScanDeviceScreen(finish: (ScanDeviceScreenResult) -> Unit) {
|
||||
val deviceManager =
|
||||
LocalContext.current.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||
fun ScanDeviceScreen(serviceId: String, finishAction: (ScanDeviceScreenResult) -> Unit) {
|
||||
val viewModel: ScanDevicesViewModel = hiltViewModel()
|
||||
val data = viewModel.data.collectAsState().value
|
||||
|
||||
val contract = ActivityResultContracts.StartIntentSenderForResult()
|
||||
val launcher = rememberLauncherForActivityResult(contract = contract) {
|
||||
val result = if (it.resultCode == Activity.RESULT_OK) {
|
||||
ScanDeviceScreenResult.SUCCESS
|
||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||
|
||||
LaunchedEffect(isScreenActive) {
|
||||
if (!isScreenActive) {
|
||||
viewModel.stopScanner()
|
||||
finishAction(ScanDeviceScreenResult.OK)
|
||||
} else {
|
||||
ScanDeviceScreenResult.CANCEL
|
||||
viewModel.startScan(serviceId)
|
||||
}
|
||||
finish(result)
|
||||
}
|
||||
|
||||
val hasBeenInvoked = remember { mutableStateOf(false) }
|
||||
if (hasBeenInvoked.value) {
|
||||
return
|
||||
val names = data.devices.map { it.displayName() }
|
||||
StringListDialog(createConfig(names) {
|
||||
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 {
|
||||
SUCCESS, CANCEL
|
||||
@Preview
|
||||
@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"?>
|
||||
<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>
|
||||
<string name="connect_to">Link with</string>
|
||||
</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('google-permissions').to('com.google.accompanist:accompanist-permissions:0.18.0')
|
||||
alias('chart').to('com.github.PhilJay:MPAndroidChart:v3.1.0')
|
||||
alias('scanner').to('no.nordicsemi.android.support.v18:scanner:1.6.0')
|
||||
|
||||
//-- Test ------------------------------------------------------------------------------
|
||||
alias('test-junit').to('junit:junit:4.13.2')
|
||||
@@ -65,7 +66,7 @@ include ':profile_csc'
|
||||
include ':profile_gls'
|
||||
include ':profile_hrs'
|
||||
include ':profile_hts'
|
||||
include ':profile_scanner'
|
||||
include ':profile_permission'
|
||||
|
||||
include ':lib_service'
|
||||
include ':lib_theme'
|
||||
@@ -78,3 +79,4 @@ if (file('../Android-BLE-Library').exists()) {
|
||||
if (file('../Android-Scanner-Compat-Library').exists()) {
|
||||
includeBuild('../Android-Scanner-Compat-Library')
|
||||
}
|
||||
include ':profile_scanner'
|
||||
|
||||
Reference in New Issue
Block a user