Skip to content

Instantly share code, notes, and snippets.

@nikiizvorski
Created June 24, 2026 07:39
Show Gist options
  • Select an option

  • Save nikiizvorski/6c7defc0a98fae9b655ce52e94e39a3d to your computer and use it in GitHub Desktop.

Select an option

Save nikiizvorski/6c7defc0a98fae9b655ce52e94e39a3d to your computer and use it in GitHub Desktop.
Nodle SDK with Foreground Service
/**
* Update settings.gradle
*/
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
url = uri("http://maven.nodle.io")
isAllowInsecureProtocol = true
}
}
}
/**
* Dependencies in build.gradle
*/
dependencies {
implementation 'io.nodle:nodlesdk-rc-lp:94207dd00f'
}
/**
* Declare your permissions in the AndroidManifest.xml
*/
<!-- Required permissions NodleSDK -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Required permissions NodleSDK extended background capabilities -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- Required permissions NodleSDK Android 12 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
/**
* Declare your service in the AndroidManifest.xml
*/
<service
android:name=".sdk.NodleService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location|connectedDevice|dataSync" />
/**
* Helper functions for Notifications
*/
val SCAN_CHANNEL_ID = "NODLE_SCAN_CHANNEL"
val SCAN_CHANNEL_NAME = "Nodle Scan"
val GENERAL_CHANNEL_ID = "NODLE_GENERAL_CHANNEL"
val GENERAL_CHANNEL_NAME = "General"
var notifyMsg = "Nodle scan..."
fun createNotificationChannels(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val scanChannel = NotificationChannel(
SCAN_CHANNEL_ID,
SCAN_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
scanChannel.setShowBadge(false)
scanChannel.setSound(null, null)
val generalChannel = NotificationChannel(
GENERAL_CHANNEL_ID,
GENERAL_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = context.getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(scanChannel)
manager?.createNotificationChannel(generalChannel)
}
}
fun buildScanNotification(context: Context, message: String, icon: Int): Notification {
val builder = NotificationCompat.Builder(context, SCAN_CHANNEL_ID)
.setSmallIcon(icon)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(false)
.setOngoing(true)
.setSound(null)
val intent: Intent? =
context.packageManager.getLaunchIntentForPackage(context.packageName)
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_MUTABLE
)
} else {
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
builder.setContentIntent(pendingIntent)
}
return builder.build()
}
/**
* Nodle SDK Helper class
*/
class NodleScan(
val applicationContext: Context
) {
private fun isIgnoreBatteryEnabled() = (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
.isIgnoringBatteryOptimizations(applicationContext.packageName)
private val TAG: String = "NodleScan"
fun init() {
Log.d(TAG, "init")
Nodle.init(applicationContext)
NodleService.nodleScan = this
updateConfig()
}
fun startService() {
Log.d(TAG,"start service")
NodleService.startService(applicationContext)
}
fun stopService() {
Log.d(TAG,"stop service")
Nodle().stop()
NodleService.stopService(applicationContext)
}
fun start(): Boolean {
Log.d(TAG,"SDK onStart")
return try {
Nodle().start("your_pk_here", "company")
Nodle().isStarted
} catch (exception: Throwable) {
Log.e("ERROR", "SDK start")
false
}
}
fun clear() {
try {
stopService()
Nodle().clear()
} catch (exception: Throwable) {
Log.e("ERROR", "SDK stop")
}
}
fun getVersion(): String {
return Nodle().version
}
fun updateConfig() {
var shouldUseHeartbeatBackground = isIgnoreBatteryEnabled()
Nodle().config("dtn.use-cellular", true)
Nodle().config("heartbeat.background-mode", shouldUseHeartbeatBackground)
Nodle().config("ble.background-mode", shouldUseHeartbeatBackground)
Nodle().config("ble.scan.duration-msec", 20000f)
Nodle().config("ble.scan.interval-msec", 60000f)
Nodle().config("ble.scan.foreground-service", true)
}
fun observeScan(): Flow<String> {
return Nodle().events
.mapNotNull { (it as? NodleBluetoothScanRecord)?.device }
.sample(800)
}
}
/**
* Foreground Service example
*/
class NodleService() : Service() {
companion object {
lateinit var nodleScan: NodleScan
private val TAG: String = "NODLE"
fun startService(context: Context) {
Log.d(TAG, "Start service")
try {
val startIntent = Intent(context, NodleService::class.java)
ContextCompat.startForegroundService(context, startIntent)
} catch (e: Throwable) {
Log.e(TAG, "SDK service start error")
}
}
fun stopService(context: Context) {
Log.d(TAG,"stopping nodle service ...")
try {
val stopIntent = Intent(context, NodleService::class.java)
context.stopService(stopIntent)
} catch (e: Throwable) {
Log.e(TAG, "SDK service stop error")
}
}
}
override fun onCreate() {
super.onCreate()
Log.d(TAG,"onCreate ")
createNotificationChannels(applicationContext)
startForeground(
1000,
buildScanNotification(applicationContext, notifyMsg, R.drawable.ic_launcher_foreground)
)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand")
if (!nodleScan.start()) {
nodleScan.stopService()
}
return START_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.d(TAG, "OnTaskRemoved")
stopSelf()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onDestroy() {
Log.d(TAG, "onDestroy ")
super.onDestroy()
}
}
/**
* Main Activity Example how to start the service
*/
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
val nodleScan = NodleScan(applicationContext)
nodleScan.init()
findViewById<Button>(R.id.button).setOnClickListener {
nodleScan.startService()
}
findViewById<Button>(R.id.button2).setOnClickListener {
nodleScan.stopService()
}
CoroutineScope(Dispatchers.Main).launch {
nodleScan.observeScan().collect {
println(it)
}
}
}
}
// ProGuard Rules for Release builds
-keep,includecode class io.nodle.** { *; } # Nodle
@nikiizvorski

nikiizvorski commented Jun 24, 2026

Copy link
Copy Markdown
Author

In addition to the Android implementation on top here is a small example how to configure the iOS SDK for local tracking too:

// set custom list for beacons to filter
let arr = ["8730c8c0-24fe-327a-3f63-623c87e24796", "8730c8c0-24fe-327a-3f63-623c87e24795"]

// config filters
nodle.config(key: "ble.filters.ibeacon-uuid", value: arr)

// start the sdk
nodle.start(devKey: "your_pk_here", tags: "company","test")

The additional configuration here is only the beacon filter to include your own UUID to be tracked and replace the ones in the list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment