mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-02-08 16:24:27 +01:00
Extract scanner to Common Libraries
This commit is contained in:
@@ -62,7 +62,6 @@ dependencies {
|
||||
implementation project(':profile_rscs')
|
||||
implementation project(':profile_scanner')
|
||||
|
||||
implementation project(':lib_permission')
|
||||
implementation project(":lib_theme")
|
||||
implementation project(":lib_utils")
|
||||
implementation project(":lib_service")
|
||||
|
||||
@@ -2,6 +2,7 @@ package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -14,70 +15,44 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
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.bps.view.BPSScreen
|
||||
import no.nordicsemi.android.cgms.view.CGMScreen
|
||||
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.permission.bonding.view.BondingScreen
|
||||
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.prx.view.PRXScreen
|
||||
import no.nordicsemi.android.rscs.view.RSCSScreen
|
||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreen
|
||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
||||
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
internal fun HomeScreen() {
|
||||
val navController = rememberNavController()
|
||||
|
||||
val viewModel = hiltViewModel<NavigationViewModel>()
|
||||
val continueAction: () -> Unit = { viewModel.finish() }
|
||||
val state = viewModel.state.collectAsState().value
|
||||
|
||||
BackHandler { viewModel.navigateUp() }
|
||||
|
||||
NavHost(navController = navController, startDestination = NavDestination.HOME.id) {
|
||||
composable(NavDestination.HOME.id) { HomeView { viewModel.navigate(it) } }
|
||||
composable(NavDestination.CSC.id) { CSCScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.HRS.id) { HRSScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.HTS.id) { HTSScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.GLS.id) { GLSScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.BPS.id) { BPSScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.PRX.id) { PRXScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.RSCS.id) { RSCSScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.CGMS.id) { CGMScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
|
||||
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen { viewModel.finish() } }
|
||||
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
||||
BluetoothNotEnabledScreen(continueAction)
|
||||
val activity = LocalContext.current as Activity
|
||||
BackHandler {
|
||||
if (navController.currentDestination?.navigatorName != NavDestination.HOME.id) {
|
||||
navController.popBackStack()
|
||||
} else {
|
||||
activity.finish()
|
||||
}
|
||||
composable(
|
||||
NavDestination.DEVICE_NOT_CONNECTED.id,
|
||||
arguments = listOf(navArgument("args") { type = NavType.StringType })
|
||||
) {
|
||||
ScanDeviceScreen(it.arguments?.getString(ARGS_KEY)!!) {
|
||||
when (it) {
|
||||
ScanDeviceScreenResult.OK -> viewModel.finish()
|
||||
ScanDeviceScreenResult.CANCEL -> viewModel.navigateUp()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
composable(NavDestination.BONDING.id) { BondingScreen(continueAction) }
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
navController.navigate(state.url)
|
||||
val goHome = { navController.navigate(NavDestination.HOME.id) }
|
||||
|
||||
NavHost(navController = navController, startDestination = NavDestination.HOME.id) {
|
||||
composable(NavDestination.HOME.id) { HomeView { goHome() } }
|
||||
composable(NavDestination.CSC.id) { CSCScreen { goHome() } }
|
||||
composable(NavDestination.HRS.id) { HRSScreen { goHome() } }
|
||||
composable(NavDestination.HTS.id) { HTSScreen { goHome() } }
|
||||
composable(NavDestination.GLS.id) { GLSScreen { goHome() } }
|
||||
composable(NavDestination.BPS.id) { BPSScreen { goHome() } }
|
||||
composable(NavDestination.PRX.id) { PRXScreen { goHome() } }
|
||||
composable(NavDestination.RSCS.id) { RSCSScreen { goHome() } }
|
||||
composable(NavDestination.CGMS.id) { CGMScreen { goHome() } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,5 @@ enum class NavDestination(val id: String, val pairingRequired: Boolean) {
|
||||
BPS("bps-screen", false),
|
||||
PRX("prx-screen", true),
|
||||
RSCS("rscs-screen", false),
|
||||
CGMS("cgms-screen", false),
|
||||
REQUEST_PERMISSION("request-permission", false),
|
||||
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available", false),
|
||||
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled", false),
|
||||
DEVICE_NOT_CONNECTED("device-not-connected/{$ARGS_KEY}", false),
|
||||
BONDING("bonding", false);
|
||||
CGMS("cgms-screen", false);
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import no.nordicsemi.android.bps.repository.BPS_SERVICE_UUID
|
||||
import no.nordicsemi.android.cgms.repository.CGMS_UUID
|
||||
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.prx.service.IMMEDIATE_ALERT_SERVICE_UUID
|
||||
import no.nordicsemi.android.rscs.service.RSCS_SERVICE_UUID
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NavigationViewModel @Inject constructor(
|
||||
private val bleScanner: NordicBleScanner,
|
||||
private val permissionHelper: PermissionHelper,
|
||||
private val selectedDevice: SelectedBluetoothDeviceHolder
|
||||
): ViewModel() {
|
||||
|
||||
val state= MutableStateFlow(NavigationTarget(NavDestination.HOME))
|
||||
private var targetDestination = NavDestination.HOME
|
||||
|
||||
fun navigate(destination: NavDestination) {
|
||||
targetDestination = destination
|
||||
navigateToNextScreen()
|
||||
}
|
||||
|
||||
fun navigateUp() {
|
||||
targetDestination = NavDestination.HOME
|
||||
state.value = NavigationTarget(NavDestination.HOME)
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
if (state.value.destination != targetDestination) {
|
||||
navigateToNextScreen()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBluetoothState(): BluetoothPermissionState {
|
||||
return if (!permissionHelper.isRequiredPermissionGranted()) {
|
||||
BluetoothPermissionState.PERMISSION_REQUIRED
|
||||
} else when (bleScanner.getBluetoothStatus()) {
|
||||
ScannerStatus.NOT_AVAILABLE -> BluetoothPermissionState.BLUETOOTH_NOT_AVAILABLE
|
||||
ScannerStatus.DISABLED -> BluetoothPermissionState.BLUETOOTH_NOT_ENABLED
|
||||
ScannerStatus.ENABLED -> selectedDevice.device?.let {
|
||||
if (targetDestination.pairingRequired && selectedDevice.isBondingRequired()) {
|
||||
BluetoothPermissionState.BONDING_REQUIRED
|
||||
} else {
|
||||
BluetoothPermissionState.READY
|
||||
}
|
||||
} ?: BluetoothPermissionState.DEVICE_NOT_CONNECTED
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToNextScreen() {
|
||||
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.BONDING_REQUIRED -> NavDestination.BONDING
|
||||
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.BPS -> BPS_SERVICE_UUID.toString()
|
||||
NavDestination.RSCS -> RSCS_SERVICE_UUID.toString()
|
||||
NavDestination.PRX -> IMMEDIATE_ALERT_SERVICE_UUID.toString()
|
||||
NavDestination.CGMS -> CGMS_UUID.toString()
|
||||
NavDestination.HOME,
|
||||
NavDestination.REQUEST_PERMISSION,
|
||||
NavDestination.BLUETOOTH_NOT_AVAILABLE,
|
||||
NavDestination.BLUETOOTH_NOT_ENABLED,
|
||||
NavDestination.BONDING,
|
||||
NavDestination.DEVICE_NOT_CONNECTED -> throw IllegalArgumentException("There is no serivce related to the destination: $destination")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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,24 +0,0 @@
|
||||
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 +0,0 @@
|
||||
<?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,41 +0,0 @@
|
||||
package no.nordicsemi.android.permission
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import no.nordicsemi.android.permission.bonding.repository.BondingStateObserver
|
||||
import no.nordicsemi.android.permission.tools.PermissionHelper
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object HiltModule {
|
||||
|
||||
@Provides
|
||||
fun createNordicBleScanner(): BluetoothAdapter? {
|
||||
return BluetoothAdapter.getDefaultAdapter()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun createSelectedBluetoothDeviceHolder(): SelectedBluetoothDeviceHolder {
|
||||
return SelectedBluetoothDeviceHolder()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun createPermissionHelper(@ApplicationContext context: Context): PermissionHelper {
|
||||
return PermissionHelper(context)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun createBondingStateObserver(@ApplicationContext context: Context): BondingStateObserver {
|
||||
return BondingStateObserver(context)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package no.nordicsemi.android.permission.bonding.repository
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import no.nordicsemi.android.service.BondingState
|
||||
|
||||
class BondingStateObserver(private val context: Context) {
|
||||
|
||||
val events: MutableSharedFlow<BondingStateChangeEvent> = MutableSharedFlow(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
fun startObserving() {
|
||||
context.applicationContext.registerReceiver(
|
||||
broadcastReceiver,
|
||||
IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
||||
)
|
||||
}
|
||||
|
||||
fun stopObserving() {
|
||||
context.applicationContext.unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
|
||||
private val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
with(intent) {
|
||||
if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
|
||||
val device = getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||
val previousBondState = getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
|
||||
val bondState = getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
|
||||
val bondTransition = "${previousBondState.toBondStateDescription()} to " + bondState.toBondStateDescription()
|
||||
Log.w("Bond state change", "${device?.address} bond state changed | $bondTransition")
|
||||
events.tryEmit(BondingStateChangeEvent(device, bondState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toBondStateDescription() = when(this) {
|
||||
BluetoothDevice.BOND_BONDED -> "BONDED"
|
||||
BluetoothDevice.BOND_BONDING -> "BONDING"
|
||||
BluetoothDevice.BOND_NONE -> "NOT BONDED"
|
||||
else -> "ERROR: $this"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class BondingStateChangeEvent(val device: BluetoothDevice?, private val bondStateValue: Int) {
|
||||
|
||||
val bondState = BondingState.create(bondStateValue)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package no.nordicsemi.android.permission.bonding.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.permission.R
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
|
||||
@Composable
|
||||
internal fun BondingErrorView() {
|
||||
ScreenSection {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.bonding_error),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BondingErrorViewPreview() {
|
||||
BondingErrorView()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package no.nordicsemi.android.permission.bonding.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.permission.R
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
|
||||
@Composable
|
||||
internal fun BondingInProgressView() {
|
||||
ScreenSection {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.bonding_in_progress),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BondingInProgressViewPreview() {
|
||||
BondingInProgressView()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package no.nordicsemi.android.permission.bonding.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.permission.bonding.viewmodel.BondingViewModel
|
||||
import no.nordicsemi.android.service.BondingState
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
fun BondingScreen(finishAction: () -> Unit) {
|
||||
val viewModel: BondingViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
|
||||
LaunchedEffect("start") {
|
||||
viewModel.bondDevice()
|
||||
}
|
||||
|
||||
when (state) {
|
||||
BondingState.BONDING -> BondingInProgressView()
|
||||
BondingState.BONDED -> finishAction()
|
||||
BondingState.NONE -> BondingErrorView()
|
||||
}.exhaustive
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package no.nordicsemi.android.permission.bonding.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.permission.R
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
|
||||
@Composable
|
||||
internal fun BondingSuccessView() {
|
||||
ScreenSection {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.bonding_success),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BondingSuccessViewPreview() {
|
||||
BondingSuccessView()
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package no.nordicsemi.android.permission.bonding.viewmodel
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.permission.bonding.repository.BondingStateObserver
|
||||
import no.nordicsemi.android.service.BondingState
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BondingViewModel @Inject constructor(
|
||||
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
||||
private val bondingStateObserver: BondingStateObserver
|
||||
) : CloseableViewModel() {
|
||||
|
||||
val state = MutableStateFlow(deviceHolder.getBondingState())
|
||||
|
||||
init {
|
||||
bondingStateObserver.events.onEach { event ->
|
||||
event.device?.let {
|
||||
if (it == deviceHolder.device) {
|
||||
state.tryEmit(event.bondState)
|
||||
} else {
|
||||
state.tryEmit(BondingState.NONE)
|
||||
}
|
||||
} ?: state.tryEmit(event.bondState)
|
||||
}.launchIn(viewModelScope)
|
||||
bondingStateObserver.startObserving()
|
||||
}
|
||||
|
||||
fun bondDevice() {
|
||||
deviceHolder.bondDevice()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
bondingStateObserver.stopObserving()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package no.nordicsemi.android.permission.tools
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import javax.inject.Inject
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class NordicBleScanner @Inject constructor(private val bleAdapter: BluetoothAdapter?) {
|
||||
|
||||
fun getBluetoothStatus(): ScannerStatus {
|
||||
return when {
|
||||
bleAdapter == null -> ScannerStatus.NOT_AVAILABLE
|
||||
bleAdapter.isEnabled -> ScannerStatus.ENABLED
|
||||
else -> ScannerStatus.DISABLED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package no.nordicsemi.android.permission.tools
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class PermissionHelper(private val context: Context) {
|
||||
|
||||
fun isRequiredPermissionGranted(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.BLUETOOTH_CONNECT
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package no.nordicsemi.android.permission.tools
|
||||
|
||||
enum class ScannerStatus {
|
||||
ENABLED, DISABLED, NOT_AVAILABLE
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package no.nordicsemi.android.permission.view
|
||||
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.permission.R
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
||||
|
||||
@Composable
|
||||
fun BluetoothNotAvailableScreen(finish: () -> Unit) {
|
||||
Column {
|
||||
CloseIconAppBar(stringResource(id = R.string.scanner__request_permission)) {
|
||||
finish()
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.scanner__bluetooth_not_available))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BluetoothNotEnabledScreen(finish: () -> Unit) {
|
||||
val contract = ActivityResultContracts.StartActivityForResult()
|
||||
val launcher = rememberLauncherForActivityResult(contract = contract, onResult = {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
finish()
|
||||
}
|
||||
})
|
||||
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.scanner__request_permission)) {
|
||||
finish()
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(id = R.string.scanner__bluetooth_not_enabled)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(id = R.string.scanner__bluetooth_open_settings_info)
|
||||
)
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Button(
|
||||
onClick = { launcher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) }
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.scanner__bluetooth_open_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package no.nordicsemi.android.permission.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.permission.R
|
||||
|
||||
@Composable
|
||||
private fun NotConnectedScreen(
|
||||
connect: () -> Unit
|
||||
) {
|
||||
NotConnectedView(connect)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotConnectedView(
|
||||
connect: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.csc_no_connection))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = { connect() }
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.csc_connect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun NotConnectedPreview() {
|
||||
NotConnectedView { }
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package no.nordicsemi.android.permission.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
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.layout.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.permission.R
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun RequestPermissionScreen(finish: () -> Unit) {
|
||||
val permissionsState = rememberMultiplePermissionsState(listOf(
|
||||
android.Manifest.permission.BLUETOOTH_CONNECT
|
||||
))
|
||||
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.scanner__request_permission)) {
|
||||
finish()
|
||||
}
|
||||
|
||||
PermissionsRequired(
|
||||
multiplePermissionsState = permissionsState,
|
||||
permissionsNotGrantedContent = { PermissionNotGranted { permissionsState.launchMultiplePermissionRequest() } },
|
||||
permissionsNotAvailableContent = { PermissionNotAvailable() }
|
||||
) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionNotGranted(onClick: () -> Unit) {
|
||||
val doNotShowRationale = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (doNotShowRationale.value) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(id = R.string.scanner__feature_not_available))
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(id = R.string.scanner__permission_rationale)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row {
|
||||
Button(modifier = Modifier.width(100.dp), onClick = { onClick() }) {
|
||||
Text(stringResource(id = R.string.scanner__button_ok))
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Button(modifier = Modifier.width(100.dp), onClick = { doNotShowRationale.value = true }) {
|
||||
Text(stringResource(id = R.string.scanner__button_nope))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionNotAvailable() {
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(id = R.string.scanner__permission_denied)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = { openPermissionSettings(context) }) {
|
||||
Text(stringResource(id = R.string.scanner__open_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPermissionSettings(context: Context) {
|
||||
startActivity(
|
||||
context,
|
||||
Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PermissionNotGrantedPreview() {
|
||||
PermissionNotGranted { }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PermissionNotAvailablePreview() {
|
||||
PermissionNotAvailable()
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package no.nordicsemi.android.permission.viewmodel
|
||||
|
||||
enum class BluetoothPermissionState {
|
||||
PERMISSION_REQUIRED,
|
||||
BLUETOOTH_NOT_AVAILABLE,
|
||||
BLUETOOTH_NOT_ENABLED,
|
||||
DEVICE_NOT_CONNECTED,
|
||||
BONDING_REQUIRED,
|
||||
READY
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?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="bonding_in_progress">Bonding in progress. Please follow instruction on the screen.</string>
|
||||
<string name="bonding_success">Bonding success. Please wait for the redirect to chosen profile screen.</string>
|
||||
<string name="bonding_error">We cannot get data from the peripheral without bonding. Please bond the device.</string>
|
||||
</resources>
|
||||
@@ -1,17 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class SelectedBluetoothDeviceHolder {
|
||||
@Singleton
|
||||
class SelectedBluetoothDeviceHolder @Inject constructor() {
|
||||
|
||||
var device: BluetoothDevice? = null
|
||||
private set
|
||||
@@ -11,14 +14,6 @@ class SelectedBluetoothDeviceHolder {
|
||||
return device?.bondState == BluetoothDevice.BOND_NONE
|
||||
}
|
||||
|
||||
fun getBondingState(): BondingState {
|
||||
return when (device?.bondState) {
|
||||
BluetoothDevice.BOND_BONDED -> BondingState.BONDED
|
||||
BluetoothDevice.BOND_BONDING -> BondingState.BONDING
|
||||
else -> BondingState.NONE
|
||||
}
|
||||
}
|
||||
|
||||
fun bondDevice() {
|
||||
device?.createBond()
|
||||
}
|
||||
@@ -31,18 +26,3 @@ class SelectedBluetoothDeviceHolder {
|
||||
device = null
|
||||
}
|
||||
}
|
||||
|
||||
enum class BondingState {
|
||||
NONE, BONDING, BONDED;
|
||||
|
||||
companion object {
|
||||
fun create(value: Int): BondingState {
|
||||
return when (value) {
|
||||
BluetoothDevice.BOND_BONDED -> BONDED
|
||||
BluetoothDevice.BOND_BONDING -> BONDING
|
||||
BluetoothDevice.BOND_NONE -> NONE
|
||||
else -> throw IllegalArgumentException("Cannot create BondingState for the value: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +73,12 @@ include ':profile_prx'
|
||||
include ':profile_rscs'
|
||||
include ':profile_scanner'
|
||||
|
||||
include ':lib_permission'
|
||||
include ':lib_service'
|
||||
include ':lib_theme'
|
||||
include ':lib_utils'
|
||||
|
||||
include ':scanner'
|
||||
|
||||
if (file('../Android-Common-Libraries').exists()) {
|
||||
includeBuild('../Android-Common-Libraries')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user