Created
June 24, 2026 07:39
-
-
Save nikiizvorski/6c7defc0a98fae9b655ce52e94e39a3d to your computer and use it in GitHub Desktop.
Nodle SDK with Foreground Service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In addition to the Android implementation on top here is a small example how to configure the iOS SDK for local tracking too:
The additional configuration here is only the beacon filter to include your own UUID to be tracked and replace the ones in the list.