Extract scanner to Common Libraries

This commit is contained in:
Sylwester Zieliński
2021-12-03 11:02:19 +01:00
parent 4d15ada6eb
commit af21919ed9
25 changed files with 26 additions and 861 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
package no.nordicsemi.android.permission.tools
enum class ScannerStatus {
ENABLED, DISABLED, NOT_AVAILABLE
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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