Skip to content

Commit ec7b67e

Browse files
authored
Merge pull request #130 from NordicPlayground/blek2-scanner
Create new BLE scanner
2 parents 1793317 + 4cb743f commit ec7b67e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2033
-1252
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ dependencies {
5050
implementation(project(":permissions-wifi"))
5151
implementation(project(":permissions-internet"))
5252
implementation(project(":permissions-notification"))
53+
implementation(project(":scanner"))
54+
55+
implementation(libs.nordic.blek.client.android)
5356

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

app/src/main/java/no/nordicsemi/android/common/test/MainActivity.kt

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ package no.nordicsemi.android.common.test
3434
import android.annotation.SuppressLint
3535
import android.os.Bundle
3636
import android.widget.Toast
37-
import androidx.activity.compose.BackHandler
3837
import androidx.activity.compose.setContent
3938
import androidx.compose.foundation.layout.Column
4039
import androidx.compose.foundation.layout.WindowInsets
@@ -44,19 +43,23 @@ import androidx.compose.foundation.layout.displayCutout
4443
import androidx.compose.foundation.layout.fillMaxHeight
4544
import androidx.compose.foundation.layout.only
4645
import androidx.compose.foundation.layout.padding
46+
import androidx.compose.foundation.layout.size
4747
import androidx.compose.foundation.rememberScrollState
4848
import androidx.compose.foundation.verticalScroll
4949
import androidx.compose.material.icons.Icons
5050
import androidx.compose.material.icons.filled.Album
5151
import androidx.compose.material.icons.filled.BrokenImage
52+
import androidx.compose.material.icons.filled.FilterList
5253
import androidx.compose.material.icons.filled.ImageSearch
5354
import androidx.compose.material.icons.filled.Navigation
5455
import androidx.compose.material.icons.filled.Settings
5556
import androidx.compose.material.icons.filled.Verified
57+
import androidx.compose.material3.CircularProgressIndicator
5658
import androidx.compose.material3.DrawerValue
5759
import androidx.compose.material3.ExperimentalMaterial3Api
5860
import androidx.compose.material3.HorizontalDivider
5961
import androidx.compose.material3.Icon
62+
import androidx.compose.material3.MaterialTheme
6063
import androidx.compose.material3.ModalDrawerSheet
6164
import androidx.compose.material3.ModalNavigationDrawer
6265
import androidx.compose.material3.NavigationBar
@@ -68,8 +71,11 @@ import androidx.compose.material3.Text
6871
import androidx.compose.material3.TopAppBarDefaults
6972
import androidx.compose.material3.rememberDrawerState
7073
import androidx.compose.runtime.Composable
74+
import androidx.compose.runtime.compositionLocalOf
7175
import androidx.compose.runtime.getValue
76+
import androidx.compose.runtime.mutableStateOf
7277
import androidx.compose.runtime.rememberCoroutineScope
78+
import androidx.compose.runtime.setValue
7379
import androidx.compose.ui.Modifier
7480
import androidx.compose.ui.graphics.vector.ImageVector
7581
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -93,13 +99,15 @@ import no.nordicsemi.android.common.test.simple.Advanced
9399
import no.nordicsemi.android.common.test.simple.AdvancedDestination
94100
import no.nordicsemi.android.common.test.simple.Hello
95101
import no.nordicsemi.android.common.test.simple.HelloDialog
102+
import no.nordicsemi.android.common.test.simple.ScannerDestinationId
96103
import no.nordicsemi.android.common.test.simple.Settings
97104
import no.nordicsemi.android.common.test.simple.SettingsDestination
98105
import no.nordicsemi.android.common.test.tab.SecondDestinations
99106
import no.nordicsemi.android.common.test.tab.ThirdDestination
100107
import no.nordicsemi.android.common.test.tab.ThirdTab
101108
import no.nordicsemi.android.common.theme.NordicActivity
102109
import no.nordicsemi.android.common.theme.NordicTheme
110+
import no.nordicsemi.android.common.ui.view.AppBarIcon
103111
import no.nordicsemi.android.common.ui.view.NavigationDrawerDividerDefaults
104112
import no.nordicsemi.android.common.ui.view.NavigationDrawerTitle
105113
import no.nordicsemi.android.common.ui.view.NavigationDrawerTitleDefaults
@@ -108,6 +116,9 @@ import no.nordicsemi.android.common.ui.view.NordicLogo
108116

109117
data class Item(val title: String, val destinationId: DestinationId<Unit, *>, val icon: ImageVector)
110118

119+
val LocalScanningState = compositionLocalOf { mutableStateOf(false) }
120+
val LocalFilterState = compositionLocalOf { mutableStateOf(false) }
121+
111122
val Tabs = createSimpleDestination("tabs")
112123
val FirstTab = createSimpleDestination("first_tab")
113124
val SecondTab = createSimpleDestination("second_tab")
@@ -141,6 +152,8 @@ class MainActivity : NordicActivity() {
141152
val drawerState = rememberDrawerState(DrawerValue.Closed)
142153
val scope = rememberCoroutineScope()
143154

155+
var isFilterOpen by LocalFilterState.current
156+
144157
ModalNavigationDrawer(
145158
drawerState = drawerState,
146159
drawerContent = {
@@ -213,15 +226,32 @@ class MainActivity : NordicActivity() {
213226
topBar = {
214227
NordicLargeAppBar(
215228
title = { Text(text = stringResource(id = R.string.title_main)) },
216-
showBackButton = listOf(Hello, HelloDialog).contains(
217-
currentDestination
229+
showBackButton = currentDestination in listOf(
230+
Hello,
231+
HelloDialog,
232+
ScannerDestinationId
218233
),
219234
onNavigationButtonClick = { navigator.navigateUp() },
220235
onHamburgerButtonClick = {
221236
scope.launch { drawerState.open() }
222237
},
223238
scrollBehavior = scrollBehavior,
224239
actions = {
240+
if (navigator.isInHierarchy(ScannerDestinationId)
241+
.collectAsStateWithLifecycle().value) {
242+
if (LocalScanningState.current.value) {
243+
CircularProgressIndicator(
244+
modifier = Modifier.padding(4.dp).size(24.dp),
245+
color = MaterialTheme.colorScheme.onPrimary,
246+
strokeWidth = 2.dp,
247+
)
248+
}
249+
AppBarIcon(
250+
imageVector = Icons.Default.FilterList,
251+
contentDescription = null,
252+
onClick = { isFilterOpen = true }
253+
)
254+
}
225255
val context = LocalContext.current
226256
LoggerAppBarIcon(
227257
onClick = {

scanner/src/main/java/no/nordicsemi/android/kotlin/ble/ui/scanner/HiltModule.kt renamed to app/src/main/java/no/nordicsemi/android/common/test/main/di/CentralManagerModule.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, Nordic Semiconductor
2+
* Copyright (c) 2025, Nordic Semiconductor
33
* All rights reserved.
44
*
55
* Redistribution and use in source and binary forms, with or without modification, are
@@ -29,25 +29,27 @@
2929
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030
*/
3131

32-
package no.nordicsemi.android.kotlin.ble.ui.scanner
32+
package no.nordicsemi.android.common.test.main.di
3333

3434
import android.content.Context
3535
import dagger.Module
3636
import dagger.Provides
3737
import dagger.hilt.InstallIn
3838
import dagger.hilt.android.qualifiers.ApplicationContext
3939
import dagger.hilt.components.SingletonComponent
40-
import no.nordicsemi.android.kotlin.ble.scanner.BleScanner
40+
import kotlinx.coroutines.CoroutineScope
41+
import no.nordicsemi.kotlin.ble.client.android.CentralManager
42+
import no.nordicsemi.kotlin.ble.client.android.native
4143

4244
@Module
4345
@InstallIn(SingletonComponent::class)
44-
internal class HiltModule {
46+
object CentralManagerModule {
4547

4648
@Provides
47-
fun providesScanner(
48-
@ApplicationContext
49-
context: Context
49+
fun provideCentralManager(
50+
@ApplicationContext context: Context,
51+
scope: CoroutineScope
5052
): CentralManager {
51-
return BleScanner(context)
53+
return CentralManager.Factory.native(context, scope)
5254
}
5355
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package no.nordicsemi.android.common.test.main.di
2+
3+
import dagger.Module
4+
import dagger.Provides
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
import kotlinx.coroutines.CoroutineScope
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.SupervisorJob
10+
11+
@Module
12+
@InstallIn(SingletonComponent::class)
13+
class CoroutineScopeModule {
14+
15+
@Provides
16+
fun applicationScope() = CoroutineScope(SupervisorJob() + Dispatchers.IO)
17+
}

app/src/main/java/no/nordicsemi/android/common/test/main/page/BasicViewsPage.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
6868
import no.nordicsemi.android.common.navigation.Navigator
6969
import no.nordicsemi.android.common.test.R
7070
import no.nordicsemi.android.common.test.simple.Hello
71+
import no.nordicsemi.android.common.test.simple.ScannerDestinationId
7172
import no.nordicsemi.android.common.theme.NordicTheme
7273
import no.nordicsemi.android.common.ui.view.NordicSliderDefaults
7374
import no.nordicsemi.android.common.ui.view.PagerViewItem
@@ -76,7 +77,13 @@ import javax.inject.Inject
7677
val BasicsPage = PagerViewItem("Basics") {
7778
val vm = hiltViewModel<BasicPageViewModel>()
7879

79-
BasicViewsScreen(onOpenSimple = { vm.openSimple() },)
80+
BasicViewsScreen(
81+
onOpenSimple = { vm.openSimple() },
82+
onOpenScanner = {
83+
// Navigate to the scanner destination
84+
vm.openScanner()
85+
},
86+
)
8087
}
8188

8289
@HiltViewModel
@@ -88,11 +95,17 @@ class BasicPageViewModel @Inject constructor(
8895
fun openSimple() {
8996
navigator.navigateTo(Hello, 1)
9097
}
98+
99+
fun openScanner() {
100+
// Navigate to the scanner destination
101+
navigator.navigateTo(ScannerDestinationId)
102+
}
91103
}
92104

93105
@Composable
94106
private fun BasicViewsScreen(
95107
onOpenSimple: () -> Unit,
108+
onOpenScanner: () -> Unit,
96109
) {
97110
Column(
98111
modifier = Modifier
@@ -107,6 +120,10 @@ private fun BasicViewsScreen(
107120
) {
108121
Text(text = stringResource(id = R.string.action_simple))
109122
}
123+
124+
Button(onClick = { onOpenScanner() }) {
125+
Text(text = stringResource(id = R.string.action_scanner))
126+
}
110127
}
111128

112129
BasicViewsScreen()
@@ -256,6 +273,7 @@ private fun ContentPreview() {
256273
NordicTheme {
257274
BasicViewsScreen(
258275
onOpenSimple = {},
276+
onOpenScanner = {},
259277
)
260278
}
261279
}

app/src/main/java/no/nordicsemi/android/common/test/simple/Hello.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private val HelloDialogDestination = defineDialogDestination(HelloDialog) {
106106
)
107107
}
108108

109-
val HelloDestinations = HelloDestination + HelloDialogDestination
109+
val HelloDestinations = HelloDestination + HelloDialogDestination + ScannerDestination
110110

111111
@Composable
112112
private fun HelloScreen(
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright (c) 2025, Nordic Semiconductor
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without modification, are
6+
* permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice, this list of
9+
* conditions and the following disclaimer.
10+
*
11+
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
12+
* of conditions and the following disclaimer in the documentation and/or other materials
13+
* provided with the distribution.
14+
*
15+
* 3. Neither the name of the copyright holder nor the names of its contributors may be
16+
* used to endorse or promote products derived from this software without specific prior
17+
* written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22+
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
26+
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
27+
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
29+
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package no.nordicsemi.android.common.test.simple
33+
34+
import androidx.compose.material.icons.Icons
35+
import androidx.compose.material.icons.filled.MonitorHeart
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.runtime.setValue
38+
import androidx.hilt.navigation.compose.hiltViewModel
39+
import no.nordicsemi.android.common.navigation.createDestination
40+
import no.nordicsemi.android.common.navigation.defineDestination
41+
import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel
42+
import no.nordicsemi.android.common.scanner.data.CustomFilter
43+
import no.nordicsemi.android.common.scanner.data.OnlyNearby
44+
import no.nordicsemi.android.common.scanner.data.OnlyWithNames
45+
import no.nordicsemi.android.common.scanner.data.WithServiceUuid
46+
import no.nordicsemi.android.common.scanner.rememberFilterState
47+
import no.nordicsemi.android.common.scanner.view.FilterDialog
48+
import no.nordicsemi.android.common.scanner.view.ScannerView
49+
import no.nordicsemi.android.common.test.LocalFilterState
50+
import no.nordicsemi.android.common.test.LocalScanningState
51+
import no.nordicsemi.android.common.test.R
52+
import no.nordicsemi.kotlin.ble.client.android.ScanResult
53+
import no.nordicsemi.kotlin.ble.core.util.fromShortUuid
54+
import kotlin.uuid.ExperimentalUuidApi
55+
import kotlin.uuid.Uuid
56+
57+
val ScannerDestinationId = createDestination<Unit, ScanResult>("ble-scanner")
58+
59+
@OptIn(ExperimentalUuidApi::class)
60+
val ScannerDestination = defineDestination(ScannerDestinationId) {
61+
val navigationVM = hiltViewModel<SimpleNavigationViewModel>()
62+
63+
// Use ScannerScreen if you need an App Bar included.
64+
// ScannerScreen(
65+
// cancellable = true,
66+
// state = rememberFilterState(
67+
// dynamicFilters = listOf(
68+
// OnlyNearby(),
69+
// OnlyWithNames(),
70+
// WithServiceUuid(
71+
// title = R.string.filter_hrm,
72+
// icon = Icons.Default.MonitorHeart,
73+
// uuid = Uuid.fromShortUuid(0x180D), // Heart Rate Service UUID,
74+
// isInitiallySelected = true
75+
// )
76+
// )
77+
// ),
78+
// onResultSelected = {
79+
// when (it) {
80+
// is DeviceSelected -> {
81+
// navigationVM.navigateUpWithResult(ScannerDestinationId, it.scanResult)
82+
// }
83+
//
84+
// ScanningCancelled -> {
85+
// navigationVM.navigateUp()
86+
// }
87+
// }
88+
// }
89+
// )
90+
91+
// Or Scanner View and Filter Dialog if you control it from own App Bar.
92+
var isScanning by LocalScanningState.current
93+
var isFilterOpen by LocalFilterState.current
94+
val state = rememberFilterState(
95+
// filter = {
96+
// ServiceUuid(Uuid.fromShortUuid(0x180D))
97+
// },
98+
dynamicFilters = listOf(
99+
OnlyNearby(),
100+
OnlyWithNames(),
101+
WithServiceUuid(
102+
title = R.string.filter_hrm,
103+
icon = Icons.Default.MonitorHeart,
104+
uuid = Uuid.fromShortUuid(0x180D), // Heart Rate Service UUID,
105+
isInitiallySelected = true
106+
),
107+
CustomFilter(
108+
title = R.string.filter_show_all,
109+
predicate = { selected, result, _ ->
110+
if (selected) {
111+
!result.advertisingData.serviceUuids.contains(Uuid.fromShortUuid(0x180D))
112+
} else {
113+
true
114+
}
115+
}
116+
)
117+
)
118+
)
119+
120+
ScannerView(
121+
onScanResultSelected = {
122+
navigationVM.navigateUpWithResult(ScannerDestinationId, it)
123+
},
124+
state = state,
125+
onScanningStateChanged = {
126+
isScanning = it
127+
},
128+
)
129+
130+
if (isFilterOpen) {
131+
FilterDialog(
132+
state = state,
133+
onDismissRequest = { isFilterOpen = false },
134+
)
135+
}
136+
}

0 commit comments

Comments
 (0)