mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-01-26 18:14:23 +01:00
Refactoring & CR fixes
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
14
profile_scanner/src/main/AndroidManifest.xml
Normal file
14
profile_scanner/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="no.nordicsemi.android.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>
|
||||
@@ -0,0 +1,40 @@
|
||||
package no.nordicsemi.android.scanner
|
||||
|
||||
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.scanner.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(
|
||||
@ApplicationContext context: Context,
|
||||
bluetoothAdapter: BluetoothAdapter?
|
||||
): SelectedBluetoothDeviceHolder {
|
||||
return SelectedBluetoothDeviceHolder(
|
||||
context,
|
||||
bluetoothAdapter
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun createPermissionHelper(@ApplicationContext context: Context): PermissionHelper {
|
||||
return PermissionHelper(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package no.nordicsemi.android.scanner.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package no.nordicsemi.android.scanner.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.scanner.tools
|
||||
|
||||
enum class ScannerStatus {
|
||||
ENABLED, DISABLED, NOT_AVAILABLE
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package no.nordicsemi.android.scanner.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.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.scanner.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(
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary),
|
||||
onClick = { launcher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) }
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.scanner__bluetooth_open_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package no.nordicsemi.android.scanner.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.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
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.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.scanner.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(
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary),
|
||||
onClick = { connect() }
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.csc_connect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun NotConnectedPreview() {
|
||||
NotConnectedView { }
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package no.nordicsemi.android.scanner.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.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.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.scanner.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(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()
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun ScanDeviceScreen(finish: (ScanDeviceScreenResult) -> Unit) {
|
||||
val deviceManager =
|
||||
LocalContext.current.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||
|
||||
val contract = ActivityResultContracts.StartIntentSenderForResult()
|
||||
val launcher = rememberLauncherForActivityResult(contract = contract) {
|
||||
val result = if (it.resultCode == Activity.RESULT_OK) {
|
||||
ScanDeviceScreenResult.SUCCESS
|
||||
} else {
|
||||
ScanDeviceScreenResult.CANCEL
|
||||
}
|
||||
finish(result)
|
||||
}
|
||||
|
||||
val hasBeenInvoked = remember { mutableStateOf(false) }
|
||||
if (hasBeenInvoked.value) {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package no.nordicsemi.android.scanner.viewmodel
|
||||
|
||||
enum class BluetoothPermissionState {
|
||||
PERMISSION_REQUIRED,
|
||||
BLUETOOTH_NOT_AVAILABLE,
|
||||
BLUETOOTH_NOT_ENABLED,
|
||||
DEVICE_NOT_CONNECTED,
|
||||
READY
|
||||
}
|
||||
26
profile_scanner/src/main/res/values/strings.xml
Normal file
26
profile_scanner/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="scanner__devices_list">BLE devices</string>
|
||||
|
||||
<string name="scanner__permission_rationale">The location permission is required when using Bluetooth LE, because surrounding devices can expose user\'s location. Please grant the permission.</string>
|
||||
<string name="scanner__permission_denied">Location permission denied. Please, grant us access on the Settings screen.</string>
|
||||
<string name="scanner__open_settings">Open settings</string>
|
||||
<string name="scanner__feature_not_available">Feature not available</string>
|
||||
|
||||
<string name="scanner__list_of_devices">List of devices</string>
|
||||
<string name="scanner__error">Scanning failed due to technical reason.</string>
|
||||
<string name="scanner__no_name">Name: NONE</string>
|
||||
|
||||
<string name="csc_no_connection">No device connected</string>
|
||||
<string name="csc_connect">Connect</string>
|
||||
|
||||
<string name="scanner__button_ok">Grant</string>
|
||||
<string name="scanner__button_nope">Deny</string>
|
||||
|
||||
<string name="scanner__request_permission">Request permission</string>
|
||||
|
||||
<string name="scanner__bluetooth_not_available">Bluetooth not available.</string>
|
||||
<string name="scanner__bluetooth_not_enabled">Bluetooth not enabled.</string>
|
||||
<string name="scanner__bluetooth_open_settings_info">To enable Bluetooth please open settings.</string>
|
||||
<string name="scanner__bluetooth_open_settings">Open settings</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
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