Skip to content

Create new BLE scanner #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 62 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
2412f1b
New BLE scanner module.
himalia416 May 8, 2025
d35a73d
Scanner and show scan result.
himalia416 May 8, 2025
6c64f82
Show scan result
himalia416 May 8, 2025
667ee50
Added Rssi icon
himalia416 May 9, 2025
58da965
Scanning filter ui
himalia416 May 9, 2025
3ef0725
Scan filters.
himalia416 May 12, 2025
fad8de4
Removed unused code
himalia416 May 13, 2025
8129631
update the scan result when filter is applied.
himalia416 May 13, 2025
a264f08
Reload with already applied filters
himalia416 May 13, 2025
acc6d3b
Filter ui
himalia416 May 14, 2025
54e0e39
Filter results.
himalia416 May 15, 2025
399d82e
On filter selection color.
himalia416 May 15, 2025
85ce4b2
Sort by filter
himalia416 May 16, 2025
6b12dd8
Sort by filter fix
himalia416 May 19, 2025
beaf8c3
Place null name to the last in sorted list.
himalia416 May 19, 2025
6ac4f29
Extracted to locally scoped functions.
himalia416 May 19, 2025
f928840
Separated to filter event.
himalia416 May 19, 2025
9ee1942
clean up
himalia416 May 19, 2025
fadccf8
Show filter icon only if show filter option.
himalia416 May 19, 2025
41bf3f0
Fixed multiple filter result.
himalia416 May 19, 2025
2601f69
Clean up events and scan filter.
himalia416 May 19, 2025
6025dc4
padding
himalia416 May 19, 2025
34b1011
Clear dropdown label on filter reset
himalia416 May 20, 2025
5784654
rename
himalia416 May 20, 2025
0ebbfd9
Added scan filter in the ui state.
himalia416 May 20, 2025
0b9e917
refactoring
himalia416 May 20, 2025
5fb6b18
Preserve filters on reload or recomposition
himalia416 May 20, 2025
233c623
Added uuid option in scanning
himalia416 May 20, 2025
00e5244
Removed duplicate method
himalia416 May 20, 2025
ee44c80
refactoring
himalia416 May 20, 2025
4c2a14e
scanning error states
himalia416 May 20, 2025
b350c79
Changed Button color.
himalia416 May 23, 2025
f41b2b9
Added filter state to draw in the ui.
himalia416 May 25, 2025
b86b71b
filter enable option
himalia416 May 25, 2025
fcfea7c
filter config
himalia416 May 25, 2025
4069f85
Show sortBy filter option
himalia416 May 25, 2025
a47c0de
Separate files
himalia416 May 26, 2025
06bb53b
Fixed group by dropdown
himalia416 May 26, 2025
b1f32d1
Rename
himalia416 May 26, 2025
49265a4
Added service uuid in the filter config.
himalia416 May 26, 2025
ec1acc2
Scanning error state
himalia416 May 26, 2025
69ba58d
Added default setting
himalia416 May 27, 2025
6caa328
Added scanner destination params.
himalia416 May 27, 2025
07139ab
clean up dependencies
himalia416 May 27, 2025
30954c4
Added everything to scanner screen
himalia416 May 27, 2025
d6fe9dc
Reflected changes in the scanner destination page.
himalia416 May 27, 2025
bf129cb
Changed filter data structure.
himalia416 May 28, 2025
219ada2
todos
himalia416 May 27, 2025
249145e
use the scan filter from user input
himalia416 May 30, 2025
a850b0f
Changes after review
himalia416 Jun 2, 2025
403c4d4
clean up dependency
himalia416 Jun 2, 2025
5cb4a35
clean up
himalia416 Jun 2, 2025
ab25d55
Used string resource file
himalia416 Jun 2, 2025
b88a890
clean up
himalia416 Jun 2, 2025
44d1f06
Pass composable to display items.
himalia416 Jun 3, 2025
6af50a1
Filter view changes.
himalia416 Jun 3, 2025
77150f2
Draw filters
himalia416 Jun 6, 2025
b4478e6
Added blek library to the gradle
himalia416 Jun 6, 2025
b4f37c0
Moved to main module
himalia416 Jun 6, 2025
1b02a7c
Fixed sortby
himalia416 Jun 6, 2025
cd1c848
Params not needed
himalia416 Jun 6, 2025
8f096c6
Added scanner destination to the app module
himalia416 Jun 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ dependencies {
implementation(project(":permissions-wifi"))
implementation(project(":permissions-internet"))
implementation(project(":permissions-notification"))
implementation(project(":scanner"))

implementation(libs.nordic.blek.client.android)

implementation(libs.androidx.compose.material.iconsExtended)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Nordic Semiconductor
* Copyright (c) 2025, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
Expand Down Expand Up @@ -29,25 +29,27 @@
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package no.nordicsemi.android.kotlin.ble.ui.scanner
package no.nordicsemi.android.common.test.main.di

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.kotlin.ble.scanner.BleScanner
import kotlinx.coroutines.CoroutineScope
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.native

@Module
@InstallIn(SingletonComponent::class)
internal class HiltModule {
object CentralManagerModule {

@Provides
fun providesScanner(
@ApplicationContext
context: Context
fun provideCentralManager(
@ApplicationContext context: Context,
scope: CoroutineScope
): CentralManager {
return BleScanner(context)
return CentralManager.Factory.native(context, scope)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package no.nordicsemi.android.common.test.main.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob

@Module
@InstallIn(SingletonComponent::class)
class CoroutineScopeModule {

@Provides
fun applicationScope() = CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.common.test.R
import no.nordicsemi.android.common.test.simple.Hello
import no.nordicsemi.android.common.test.simple.ScannerDestinationId
import no.nordicsemi.android.common.theme.NordicTheme
import no.nordicsemi.android.common.ui.view.NordicSliderDefaults
import no.nordicsemi.android.common.ui.view.PagerViewItem
Expand All @@ -76,7 +77,13 @@ import javax.inject.Inject
val BasicsPage = PagerViewItem("Basics") {
val vm = hiltViewModel<BasicPageViewModel>()

BasicViewsScreen(onOpenSimple = { vm.openSimple() },)
BasicViewsScreen(
onOpenSimple = { vm.openSimple() },
onOpenScanner = {
// Navigate to the scanner destination
vm.openScanner()
},
)
}

@HiltViewModel
Expand All @@ -88,11 +95,17 @@ class BasicPageViewModel @Inject constructor(
fun openSimple() {
navigator.navigateTo(Hello, 1)
}

fun openScanner() {
// Navigate to the scanner destination
navigator.navigateTo(ScannerDestinationId)
}
}

@Composable
private fun BasicViewsScreen(
onOpenSimple: () -> Unit,
onOpenScanner: () -> Unit,
) {
Column(
modifier = Modifier
Expand All @@ -107,6 +120,10 @@ private fun BasicViewsScreen(
) {
Text(text = stringResource(id = R.string.action_simple))
}

Button(onClick = { onOpenScanner() }) {
Text(text = stringResource(id = R.string.action_scanner))
}
}

BasicViewsScreen()
Expand Down Expand Up @@ -256,6 +273,7 @@ private fun ContentPreview() {
NordicTheme {
BasicViewsScreen(
onOpenSimple = {},
onOpenScanner = {},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private val HelloDialogDestination = defineDialogDestination(HelloDialog) {
)
}

val HelloDestinations = HelloDestination + HelloDialogDestination
val HelloDestinations = HelloDestination + HelloDialogDestination + ScannerDestination

@Composable
private fun HelloScreen(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Nordic Semiconductor
* Copyright (c) 2025, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
Expand Down Expand Up @@ -29,27 +29,36 @@
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package no.nordicsemi.android.kotlin.ble.ui.scanner.repository
package no.nordicsemi.android.common.test.simple

import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResults
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.common.navigation.createDestination
import no.nordicsemi.android.common.navigation.defineDestination
import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel
import no.nordicsemi.android.common.scanner.DeviceSelected
import no.nordicsemi.android.common.scanner.ScannerScreen
import no.nordicsemi.android.common.scanner.ScanningCancelled
import no.nordicsemi.kotlin.ble.client.android.ScanResult

sealed class ScanningState {
val ScannerDestinationId = createDestination<Unit, ScanResult>("ble-scanner")

data object Loading : ScanningState()
val ScannerDestination = defineDestination(ScannerDestinationId) {
val navigationVM = hiltViewModel<SimpleNavigationViewModel>()

data class Error(val errorCode: Int) : ScanningState()
ScannerScreen(
cancellable = true,
onResultSelected = {
when (it) {
is DeviceSelected -> {
navigationVM.navigateUpWithResult(ScannerDestinationId, it.scanResult)
println("Device selected: ${it.scanResult}")
}

data class DevicesDiscovered(val devices: List<BleScanResults>) : ScanningState() {
val bonded: List<BleScanResults> = devices.filter { it.device.isBonded }

val notBonded: List<BleScanResults> = devices.filter { !it.device.isBonded }

fun size(): Int = bonded.size + notBonded.size

fun isEmpty(): Boolean = devices.isEmpty()
}

fun isRunning(): Boolean {
return this is Loading || this is DevicesDiscovered
}
ScanningCancelled -> {
navigationVM.navigateUp()
println("Scanning cancelled")
}
}
}
)
}
6 changes: 4 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
accompanist = "0.37.0"
androidGradlePlugin = "8.7.3"
androidGradlePlugin = "8.10.1"
androidxActivity = "1.9.3"
androidxComposeBom = "2024.12.01" # https://developer.android.com/jetpack/compose/bom/bom-mapping
androidxCore = "1.15.0"
Expand All @@ -21,7 +21,8 @@ publishPlugin = "1.3.0"
leakcanary = "2.14"

nordic-log = "2.5.0"
nordicPlugins = "2.6.1"
nordic-blek = "2.0.0-alpha02"
nordicPlugins = "2.7"
dokkaPlugin = "2.0.0"
googleServicesPlugins = "4.4.2"

Expand Down Expand Up @@ -58,6 +59,7 @@ leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", v

# Nordic
nordic-log = { group = "no.nordicsemi.android", name = "log", version.ref = "nordic-log" }
nordic-blek-client-android = { group = "no.nordicsemi.kotlin.ble", name = "client-android", version.ref = "nordic-blek" }

[plugins]
nordic-application = { id = "no.nordicsemi.android.plugin.application", version.ref = "nordicPlugins" }
Expand Down
22 changes: 9 additions & 13 deletions scanner/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,29 @@
plugins {
alias(libs.plugins.nordic.feature)
alias(libs.plugins.nordic.nexus.android)
alias(libs.plugins.kotlin.parcelize)
}

group = "no.nordicsemi.android.common"

nordicNexusPublishing {
POM_ARTIFACT_ID = "scanner"
POM_NAME = "Nordic Kotlin library for BLE server side."
POM_NAME = "Nordic library for BLE scanner."

POM_DESCRIPTION = "Nordic Android Kotlin BLE library"
POM_URL = "https://github.com/NordicPlayground/Kotlin-BLE-Library"
POM_SCM_URL = "https://github.com/NordicPlayground/Kotlin-BLE-Library"
POM_SCM_CONNECTION = "scm:[email protected]:NordicPlayground/Kotlin-BLE-Library.git"
POM_SCM_DEV_CONNECTION = "scm:[email protected]:NordicPlayground/Kotlin-BLE-Library.git"
POM_DESCRIPTION = "Nordic Android Common Libraries"
POM_URL = "https://github.com/NordicPlayground/Android-Common-Libraries"
POM_SCM_URL = "https://github.com/NordicPlayground/Android-Common-Libraries"
POM_SCM_CONNECTION = "scm:[email protected]:NordicPlayground/Android-Common-Libraries.git"
POM_SCM_DEV_CONNECTION = "scm:[email protected]:NordicPlayground/Android-Common-Libraries.git"
}

android {
namespace = "no.nordicsemi.android.common.scanner"
}

dependencies {
implementation(project(":theme"))
implementation(project(":core"))
implementation(project(":permissions-ble"))

api(libs.nordic.blek.client.android)

implementation(libs.androidx.compose.material3)
implementation(project(":ui"))
implementation(libs.nordic.blek.client.android)
implementation(libs.androidx.compose.material.iconsExtended)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2025, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package no.nordicsemi.android.common.scanner

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import no.nordicsemi.android.common.scanner.data.Filter
import no.nordicsemi.android.common.scanner.data.GroupByName
import no.nordicsemi.android.common.scanner.data.OnlyBonded
import no.nordicsemi.android.common.scanner.data.OnlyNearby
import no.nordicsemi.android.common.scanner.data.OnlyWithNames
import no.nordicsemi.android.common.scanner.data.SortBy
import no.nordicsemi.android.common.scanner.view.DeviceListItem
import no.nordicsemi.android.common.scanner.view.ScannerAppBar
import no.nordicsemi.android.common.scanner.view.ScannerView
import no.nordicsemi.android.common.scanner.viewmodel.ScannerViewModel
import no.nordicsemi.kotlin.ble.client.android.ScanResult

val Default_Filters = listOf(
OnlyWithNames(),
OnlyNearby(),
OnlyBonded(),
SortBy(),
GroupByName(),

)

@Composable
fun ScannerScreen(
title: @Composable () -> Unit = { Text(stringResource(id = R.string.scanner_screen)) },
cancellable: Boolean,
availableFilters: List<Filter> = Default_Filters,
filters: List<Filter> = emptyList(),
onResultSelected: (ScannerScreenResult) -> Unit,
deviceItem: @Composable (ScanResult) -> Unit = { scanResult ->
DeviceListItem(scanResult)
}
) {
val viewModel = hiltViewModel<ScannerViewModel>()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

// If filters to be passed to the view model.
LaunchedEffect(filters.isNotEmpty()) {
viewModel.setFilters(filters)
}

Column(
modifier = Modifier.fillMaxSize()
) {
if (cancellable) {
ScannerAppBar(
title = title,
uiState = uiState,
availableFilters = availableFilters,
onFilterSelected = viewModel::onClick,
) { onResultSelected(ScanningCancelled) }
} else {
ScannerAppBar(
title = title,
uiState = uiState,
availableFilters = availableFilters,
onFilterSelected = viewModel::onClick,
)
}

ScannerView(
uiState = uiState,
startScanning = viewModel::startScanning,
onEvent = viewModel::onClick,
onScanResultSelected = { onResultSelected(DeviceSelected(it)) },
deviceItem = deviceItem,
)
}
}
Loading