mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-01-24 09:04:20 +01:00
Modernization of modular approach
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
package no.nordicsemi.android.scanner
|
||||
|
||||
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.scanner">
|
||||
|
||||
<uses-feature android:name="android.software.companion_device_setup"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
</manifest>
|
||||
@@ -1,17 +0,0 @@
|
||||
package no.nordicsemi.android.scanner
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object HiltModule {
|
||||
|
||||
@Provides
|
||||
fun createNordicBleScanner(): BluetoothAdapter? {
|
||||
return BluetoothAdapter.getDefaultAdapter()
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package no.nordicsemi.android.scanner
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import no.nordicsemi.android.events.exhaustive
|
||||
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
||||
import no.nordicsemi.android.scanner.ui.BluetoothNotAvailableScreen
|
||||
import no.nordicsemi.android.scanner.ui.BluetoothNotEnabledScreen
|
||||
import no.nordicsemi.android.scanner.ui.NordicBleScannerViewModel
|
||||
import no.nordicsemi.android.scanner.ui.RequestPermissionScreen
|
||||
import no.nordicsemi.android.scanner.ui.ScanDeviceScreen
|
||||
import no.nordicsemi.android.scanner.ui.ScannerViewEvent
|
||||
|
||||
@Composable
|
||||
fun ScannerRoute(navController: NavController) {
|
||||
val viewModel = hiltViewModel<NordicBleScannerViewModel>()
|
||||
|
||||
val scannerStatus = viewModel.state.collectAsState().value.scannerStatus
|
||||
|
||||
Column {
|
||||
TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__devices_list)) })
|
||||
ScannerScreen(navController, scannerStatus) { viewModel.onEvent(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScannerScreen(
|
||||
navController: NavController,
|
||||
scannerStatus: ScannerStatus,
|
||||
onEvent: (ScannerViewEvent) -> Unit
|
||||
) {
|
||||
when (scannerStatus) {
|
||||
ScannerStatus.PERMISSION_REQUIRED -> RequestPermissionScreen { onEvent(ScannerViewEvent.PERMISSION_CHECKED) }
|
||||
ScannerStatus.NOT_AVAILABLE -> BluetoothNotAvailableScreen()
|
||||
ScannerStatus.DISABLED -> BluetoothNotEnabledScreen { onEvent(ScannerViewEvent.BLUETOOTH_ENABLED) }
|
||||
ScannerStatus.ENABLED -> ScanDeviceScreen(navController)
|
||||
}.exhaustive
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package no.nordicsemi.android.scanner.tools
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanResult
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
internal class NordicBleScanner @Inject constructor(private val bleAdapter: BluetoothAdapter?) {
|
||||
|
||||
val scannerResult = MutableStateFlow<ScanningResult>(DeviceListResult())
|
||||
|
||||
private var isScanning = false
|
||||
|
||||
private val scanner by lazy { bleAdapter?.bluetoothLeScanner }
|
||||
private val devices = mutableListOf<BluetoothDevice>()
|
||||
|
||||
private val scanningCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult?) {
|
||||
result?.device?.let { devices.addIfNotExist(it) }
|
||||
scannerResult.value = DeviceListResult(devices)
|
||||
}
|
||||
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
scannerResult.value = ScanningErrorResult
|
||||
}
|
||||
}
|
||||
|
||||
fun getBluetoothStatus(): ScannerStatus {
|
||||
return when {
|
||||
bleAdapter == null -> ScannerStatus.NOT_AVAILABLE
|
||||
bleAdapter.isEnabled -> ScannerStatus.ENABLED
|
||||
else -> ScannerStatus.DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
fun startScanning() {
|
||||
if (isScanning) {
|
||||
return
|
||||
}
|
||||
isScanning = true
|
||||
scanner?.startScan(scanningCallback)
|
||||
}
|
||||
|
||||
fun stopScanning() {
|
||||
if (!isScanning) {
|
||||
return
|
||||
}
|
||||
isScanning = false
|
||||
scanner?.stopScan(scanningCallback)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ScanningResult
|
||||
|
||||
data class DeviceListResult(val devices: List<BluetoothDevice> = emptyList()) : ScanningResult()
|
||||
|
||||
object ScanningErrorResult : ScanningResult()
|
||||
|
||||
private fun <T> MutableList<T>.addIfNotExist(value: T) {
|
||||
if (!contains(value)) {
|
||||
add(value)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package no.nordicsemi.android.scanner.tools
|
||||
|
||||
enum class ScannerStatus {
|
||||
PERMISSION_REQUIRED, ENABLED, DISABLED, NOT_AVAILABLE
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package no.nordicsemi.android.scanner.ui
|
||||
|
||||
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.Column
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
internal fun BluetoothNotAvailableScreen() {
|
||||
Text("Bluetooth not available.")
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BluetoothNotEnabledScreen(finish: () -> Unit) {
|
||||
val contract = ActivityResultContracts.StartActivityForResult()
|
||||
val launcher = rememberLauncherForActivityResult(contract = contract, onResult = {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
finish()
|
||||
}
|
||||
})
|
||||
|
||||
Column {
|
||||
Text(text = "Bluetooth not enabled.")
|
||||
Text(text = "To enable Bluetooth please open settings.")
|
||||
Button(onClick = { launcher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) }) {
|
||||
Text(text = "Bluetooth not available.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package no.nordicsemi.android.scanner.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import no.nordicsemi.android.events.exhaustive
|
||||
import no.nordicsemi.android.scanner.tools.NordicBleScanner
|
||||
import no.nordicsemi.android.scanner.tools.ScannerStatus
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class NordicBleScannerViewModel @Inject constructor(
|
||||
private val bleScanner: NordicBleScanner
|
||||
) : ViewModel() {
|
||||
|
||||
val state =
|
||||
MutableStateFlow(NordicBleScannerState(scannerStatus = ScannerStatus.PERMISSION_REQUIRED))
|
||||
|
||||
val scannerResult = bleScanner.scannerResult
|
||||
|
||||
fun onEvent(event: ScannerViewEvent) {
|
||||
when (event) {
|
||||
ScannerViewEvent.PERMISSION_CHECKED -> onPermissionChecked()
|
||||
ScannerViewEvent.BLUETOOTH_ENABLED -> onBluetoothEnabled()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun onPermissionChecked() {
|
||||
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
|
||||
}
|
||||
|
||||
private fun onBluetoothEnabled() {
|
||||
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
|
||||
bleScanner.startScanning()
|
||||
}
|
||||
}
|
||||
|
||||
enum class ScannerViewEvent {
|
||||
PERMISSION_CHECKED, BLUETOOTH_ENABLED
|
||||
}
|
||||
|
||||
internal data class NordicBleScannerState(
|
||||
val scannerStatus: ScannerStatus
|
||||
)
|
||||
@@ -1,119 +0,0 @@
|
||||
package no.nordicsemi.android.scanner.ui
|
||||
|
||||
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.width
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.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.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.scanner.R
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
internal fun RequestPermissionScreen(finish: () -> Unit) {
|
||||
val permissionsState = rememberMultiplePermissionsState(listOf(
|
||||
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
// android.Manifest.permission.BLUETOOTH_SCAN,
|
||||
// android.Manifest.permission.BLUETOOTH_CONNECT
|
||||
))
|
||||
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(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(id = R.string.scanner__permission_rationale))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row {
|
||||
Button(onClick = { onClick() }) {
|
||||
Text(stringResource(id = R.string.scanner__button_ok))
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(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(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,62 +0,0 @@
|
||||
package no.nordicsemi.android.scanner.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.companion.AssociationRequest
|
||||
import android.companion.BluetoothDeviceFilter
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
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.navigation.NavController
|
||||
|
||||
@Composable
|
||||
fun ScanDeviceScreen(navController: NavController,) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
|
||||
.build()
|
||||
|
||||
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
|
||||
.build()
|
||||
|
||||
val deviceManager =
|
||||
LocalContext.current.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||
|
||||
val contract = ActivityResultContracts.StartIntentSenderForResult()
|
||||
val launcher = rememberLauncherForActivityResult(contract = contract, onResult = {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
val deviceToPair: BluetoothDevice? = it.data?.getParcelableExtra(
|
||||
CompanionDeviceManager.EXTRA_DEVICE)
|
||||
navController.previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("result", deviceToPair)
|
||||
navController.popBackStack()
|
||||
}
|
||||
})
|
||||
|
||||
val hasBeenInvoked = remember { mutableStateOf(false) }
|
||||
if (hasBeenInvoked.value) {
|
||||
return
|
||||
}
|
||||
hasBeenInvoked.value = true
|
||||
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)
|
||||
} else {
|
||||
TODO("VERSION.SDK_INT < O")
|
||||
}
|
||||
}
|
||||
@@ -1,15 +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__button_ok">OK</string>
|
||||
<string name="scanner__button_nope">Nope</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>
|
||||
</resources>
|
||||
@@ -1,17 +0,0 @@
|
||||
package no.nordicsemi.android.scanner
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user