diff --git a/app/build.gradle b/app/build.gradle index 8654dc87..060252d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt index 27fe1b12..723f3419 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt @@ -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) } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt index c6513cfc..144ac172 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt @@ -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}"); } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationTarget.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationTarget.kt new file mode 100644 index 00000000..bcde30aa --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationTarget.kt @@ -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 +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt index 620659a2..f6419e15 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt @@ -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") + } } } diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt index ddefcae2..fb97f2b1 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt @@ -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 } } diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SpeedUnitRadioGroup.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SelectItemRadioGroup.kt similarity index 90% rename from lib_theme/src/main/java/no/nordicsemi/android/theme/view/SpeedUnitRadioGroup.kt rename to lib_theme/src/main/java/no/nordicsemi/android/theme/view/SelectItemRadioGroup.kt index c43d94e0..760632e4 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SpeedUnitRadioGroup.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SelectItemRadioGroup.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun SpeedUnitRadioGroup( +fun SelectItemRadioGroup( currentItem: T, items: List>, onEvent: (RadioGroupItem) -> Unit @@ -22,13 +22,13 @@ fun SpeedUnitRadioGroup( horizontalArrangement = Arrangement.SpaceEvenly ) { items.forEach { - SpeedUnitRadioButton(currentItem, it, onEvent) + SelectItemRadioButton(currentItem, it, onEvent) } } } @Composable -internal fun SpeedUnitRadioButton( +internal fun SelectItemRadioButton( selectedItem: T, displayedItem: RadioGroupItem, onEvent: (RadioGroupItem) -> Unit diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/Ext.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/Ext.kt new file mode 100644 index 00000000..2032dfdb --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/Ext.kt @@ -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) +} diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialog.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialog.kt new file mode 100644 index 00000000..bf402c17 --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialog.kt @@ -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), + ) + } + } + } + } +} diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialogConfig.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialogConfig.kt new file mode 100644 index 00000000..44f991b4 --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialogConfig.kt @@ -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 = emptyList(), + val onResult: (StringListDialogResult) -> Unit +) diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialogResult.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialogResult.kt new file mode 100644 index 00000000..57c43472 --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialogResult.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.theme.view.dialog + +sealed class StringListDialogResult + +data class ItemSelectedResult(val index: Int): StringListDialogResult() + +object FlowCanceled : StringListDialogResult() diff --git a/lib_theme/src/main/res/values/strings.xml b/lib_theme/src/main/res/values/strings.xml index 93e85944..b5bbd16c 100644 --- a/lib_theme/src/main/res/values/strings.xml +++ b/lib_theme/src/main/res/values/strings.xml @@ -2,6 +2,9 @@ nRF Toolbox + Dialog + CANCEL + Close the application. Close the current screen. diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt index 3aeb1bd4..b36f27de 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt @@ -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 diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCDataHolder.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCDataHolder.kt index 810c113f..2a42e02a 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCDataHolder.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCDataHolder.kt @@ -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)) } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt index a9e17354..4f749bae 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt @@ -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") diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCConnectedView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCConnectedView.kt index 2b5265ba..fe1df765 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCConnectedView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCConnectedView.kt @@ -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)) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt index 90a40387..d1c60edb 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt @@ -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() diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt index a4cc852b..c08a4138 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt @@ -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, 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) {}) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt index a61d02ee..721ab560 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt @@ -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()) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt index 5314513b..44644f7b 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt @@ -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) } ) } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt index bb9278e7..33bdbb4b 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt @@ -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() + } } diff --git a/profile_csc/src/main/res/values/strings.xml b/profile_csc/src/main/res/values/strings.xml index c38b30c3..b5eb3b86 100644 --- a/profile_csc/src/main/res/values/strings.xml +++ b/profile_csc/src/main/res/values/strings.xml @@ -2,13 +2,15 @@ Cyclic and speed cadence - Speed - Cadence - Distance - Total Distance - Gear Ratio + Select wheel size - Wheel size + Speed + Cadence + Distance + Total Distance + Gear Ratio + + Wheel size 60–622 diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt index 2adb1c1e..4387d4dd 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt @@ -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") diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt index 41e9863f..63c40a97 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt @@ -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)) } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt index 590ac0d5..cab9316d 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt @@ -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") diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt index e54b14bb..14b7c7f5 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt @@ -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") /** diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt index 4337f403..0882738a 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt @@ -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)) } } diff --git a/profile_permission/build.gradle b/profile_permission/build.gradle new file mode 100644 index 00000000..e1151501 --- /dev/null +++ b/profile_permission/build.gradle @@ -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 +} diff --git a/profile_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt b/profile_permission/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt similarity index 94% rename from profile_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt rename to profile_permission/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt index 48ba3cea..29f4fc83 100644 --- a/profile_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt +++ b/profile_permission/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt @@ -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 diff --git a/profile_permission/src/main/AndroidManifest.xml b/profile_permission/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1ac23adc --- /dev/null +++ b/profile_permission/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt similarity index 66% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt index 8bb0dd5f..f7c54249 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt @@ -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 diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/NordicBleScanner.kt similarity index 91% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/tools/NordicBleScanner.kt index b4bd5183..f6eabd8c 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/NordicBleScanner.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.scanner.tools +package no.nordicsemi.android.permission.tools import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/PermissionHelper.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/PermissionHelper.kt similarity index 92% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/PermissionHelper.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/tools/PermissionHelper.kt index f96447c7..5c0cd743 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/PermissionHelper.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/PermissionHelper.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.scanner.tools +package no.nordicsemi.android.permission.tools import android.Manifest import android.content.Context diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/ScannerStatus.kt similarity index 58% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/tools/ScannerStatus.kt index 4c884cab..129126c2 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/ScannerStatus.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.scanner.tools +package no.nordicsemi.android.permission.tools enum class ScannerStatus { ENABLED, DISABLED, NOT_AVAILABLE diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/view/BluetoothNotAvailableScreen.kt similarity index 97% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/view/BluetoothNotAvailableScreen.kt index b76dae26..9ae17a8d 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/view/BluetoothNotAvailableScreen.kt @@ -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 diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/NotConnectedView.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/view/NotConnectedView.kt similarity index 94% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/NotConnectedView.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/view/NotConnectedView.kt index f3790a3b..1d8e9cc3 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/NotConnectedView.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/view/NotConnectedView.kt @@ -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( diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt similarity index 98% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt index 3f31af98..df68ea9c 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt @@ -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) diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/BluetoothPermissionState.kt b/profile_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt similarity index 75% rename from profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/BluetoothPermissionState.kt rename to profile_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt index 6baab097..9444f576 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/BluetoothPermissionState.kt +++ b/profile_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.scanner.viewmodel +package no.nordicsemi.android.permission.viewmodel enum class BluetoothPermissionState { PERMISSION_REQUIRED, diff --git a/profile_permission/src/main/res/values/strings.xml b/profile_permission/src/main/res/values/strings.xml new file mode 100644 index 00000000..a12e9a62 --- /dev/null +++ b/profile_permission/src/main/res/values/strings.xml @@ -0,0 +1,26 @@ + + + BLE devices + + The location permission is required when using Bluetooth LE, because surrounding devices can expose user\'s location. Please grant the permission. + Location permission denied. Please, grant us access on the Settings screen. + Open settings + Feature not available + + List of devices + Scanning failed due to technical reason. + Name: NONE + + No device connected + Connect + + Grant + Deny + + Request permission + + Bluetooth not available. + Bluetooth not enabled. + To enable Bluetooth please open settings. + Open settings + diff --git a/profile_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt b/profile_permission/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt similarity index 88% rename from profile_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt rename to profile_permission/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt index 5528248f..b91ce273 100644 --- a/profile_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt +++ b/profile_permission/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.scanner +package no.nordicsemi.android.permission import org.junit.Test diff --git a/profile_scanner/build.gradle b/profile_scanner/build.gradle index e1151501..7e244009 100644 --- a/profile_scanner/build.gradle +++ b/profile_scanner/build.gradle @@ -6,6 +6,7 @@ dependencies { implementation project(":lib_theme") implementation project(":lib_service") + implementation libs.scanner implementation libs.material implementation libs.google.permissions diff --git a/profile_scanner/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt b/profile_scanner/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..29f4fc83 --- /dev/null +++ b/profile_scanner/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/profile_scanner/src/main/AndroidManifest.xml b/profile_scanner/src/main/AndroidManifest.xml index 9a69f4cb..ba6774b4 100644 --- a/profile_scanner/src/main/AndroidManifest.xml +++ b/profile_scanner/src/main/AndroidManifest.xml @@ -1,14 +1,8 @@ - - - - + package="no.nordicsemi.android.scanner" > - - \ No newline at end of file diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/data/ScanDevicesData.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/data/ScanDevicesData.kt new file mode 100644 index 00000000..144610d3 --- /dev/null +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/data/ScanDevicesData.kt @@ -0,0 +1,22 @@ +package no.nordicsemi.android.scanner.data + +import android.bluetooth.BluetoothDevice + +data class ScanDevicesData( + val devices: List = emptyList() +) { + + fun copyWithNewDevice(device: BluetoothDevice): ScanDevicesData { + if (devices.contains(device)) { + return this + } + val newDevices = devices + device + return copy(devices = newDevices) + } + + fun copyWithNewDevices(bleDevices: List): ScanDevicesData { + val filteredDevice = bleDevices.filter { !devices.contains(it) } + val newDevices = devices + filteredDevice + return copy(devices = newDevices) + } +} diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt index add8df59..f06cd460 100644 --- a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt @@ -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, 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) } diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreenResult.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreenResult.kt new file mode 100644 index 00000000..912c02cf --- /dev/null +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreenResult.kt @@ -0,0 +1,5 @@ +package no.nordicsemi.android.scanner.view + +enum class ScanDeviceScreenResult { + OK, CANCEL +} diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDevicesViewEvent.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDevicesViewEvent.kt new file mode 100644 index 00000000..2b94f5fa --- /dev/null +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDevicesViewEvent.kt @@ -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 +} diff --git a/profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/ScanDevicesViewModel.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/ScanDevicesViewModel.kt new file mode 100644 index 00000000..5ab1c607 --- /dev/null +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/ScanDevicesViewModel.kt @@ -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) { + 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 = ArrayList() + val uuid = ParcelUuid.fromString(serviceId) + filters.add(ScanFilter.Builder().setServiceUuid(uuid).build()) + + scanner.startScan(filters, settings, scanCallback) + } + + fun stopScanner() { + scanner.stopScan(scanCallback) + } +} diff --git a/profile_scanner/src/main/res/drawable/ic_bluetooth.xml b/profile_scanner/src/main/res/drawable/ic_bluetooth.xml new file mode 100644 index 00000000..afd2c0ca --- /dev/null +++ b/profile_scanner/src/main/res/drawable/ic_bluetooth.xml @@ -0,0 +1,10 @@ + + + diff --git a/profile_scanner/src/main/res/values/strings.xml b/profile_scanner/src/main/res/values/strings.xml index a12e9a62..965fac8d 100644 --- a/profile_scanner/src/main/res/values/strings.xml +++ b/profile_scanner/src/main/res/values/strings.xml @@ -1,26 +1,4 @@ - BLE devices - - The location permission is required when using Bluetooth LE, because surrounding devices can expose user\'s location. Please grant the permission. - Location permission denied. Please, grant us access on the Settings screen. - Open settings - Feature not available - - List of devices - Scanning failed due to technical reason. - Name: NONE - - No device connected - Connect - - Grant - Deny - - Request permission - - Bluetooth not available. - Bluetooth not enabled. - To enable Bluetooth please open settings. - Open settings + Link with diff --git a/profile_scanner/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt b/profile_scanner/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt new file mode 100644 index 00000000..b91ce273 --- /dev/null +++ b/profile_scanner/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 2222c137..91b53830 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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'