MainApplication.kt
We set these initializations here instead of in an Activity in order to accommodate an app with a background work for the scanning.
For that work not to be killed by the system, it is implemented as a foreground service.
package my.package
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import my.package.ble.DaemonConfig
import my.package.ble.NotificationHelper.SERVICE_CHANNEL_ID
import my.package.ble.ScannerManager
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
// delegated to a ble component, to keep the pure app module clean of direct accesses to the library
ScannerManager.init(this)
DaemonConfig.init(this, EventListener(this), MainActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// create a notification channel, mandatory for the Foreground Service
val notificationManager = NotificationManagerCompat.from(this)
val serviceChannel = NotificationChannel(
SERVICE_CHANNEL_ID,
"Bluetooth beacon listening service in background",
NotificationManager.IMPORTANCE_LOW // "no sound"
)
serviceChannel.description = "Locates nearby beacons" // optional
notificationManager.createNotificationChannel(serviceChannel)
}
}
}
MainActivity.kt
How screens and layouts are implemented (XML, Jetpack Compose, ...) is out of scope of the samples.
An
(UI)
marker is used in the code comments to denote this point.
package my.package
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import my.package.ble.Configuration
import my.package.ble.IStatusCallback
import my.package.ble.ScannerManager
class MainActivity : ComponentActivity() {
// actions are delegated to a component
private lateinit var mScannerManager: ScannerManager
override fun onCreate(savedInstanceState: Bundle?) {
if (BuildConfig.DEBUG) Log.d(TAG, "lifecycle onCreate")
super.onCreate(savedInstanceState)
mScannerManager = ScannerManager(this)
// ... (UI) setup your screen layout
// typically, for this sample, with start and stop buttons to act on the scanning,
// respectively associated with onClickSetup() and onClickTeardown()
}
override fun onResume() {
if (BuildConfig.DEBUG) Log.d(TAG, "lifecycle onResume")
super.onResume()
mScannerManager.requestScanStatus(mStatusCallback)
}
private val mStatusCallback = object : IStatusCallback {
override fun onResult(isScanning: Boolean) {
if (BuildConfig.DEBUG) Log.d(TAG, "onResult: isScanning $isScanning")
// ... (UI) may need to update some elements, like button properties (text, enabled, ...)
}
override fun onError(message: String) {
if (BuildConfig.DEBUG) Log.d(TAG, "onError $message")
// ... (UI) inform of the failure
}
}
private fun onClickSetup() {
Log.i(TAG, "onClickSetUp")
if (mScannerManager.isBluetoothDisabled) {
// ... (UI) inform that the app can't work at all if the Bluetooth is off
} else {
if (mScannerManager.isLocationDisabled) {
// ... (UI) inform that no detection will be accessible until Location is enabled
return // useless to insist
}
// ... (UI) according to a CheckBox, may set Configuration.instance.preventDoze = true
// ... (UI) according to a CheckBox, may set Configuration.instance.verbose = true
mScannerManager.setUp(mStatusCallback)
}
}
private fun onClickTeardown() {
Log.i((TAG, "onClickTearDown")
mScannerManager.tearDown(mStatusCallback)
}
private companion object {
private val TAG = MainActivity::class.java.simpleName
}
}
EventListener.kt
package my.package
import android.content.Context
import my.package.ble.IEventListener
internal class EventListener(private val mContext: Context) : IEventListener {
override fun onEvent(line: String) {
// TODO implement your custom event persistence.
// For instance, append the line on a local file and schedule a periodic upload to a central backend.
}
}
NotificationHelper.kt
package my.package.ble
import android.app.Activity
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import my.package.R
object NotificationHelper {
const val SERVICE_CHANNEL_ID = "ServiceChanId"
const val SERVICE_NOTIFICATION_ID = 1
fun createNotification(context: Context, activityClass: Class<out Activity?>, isLocationDisabled: Boolean): Notification {
val notificationBuilder = NotificationCompat.Builder(context, SERVICE_CHANNEL_ID)
val pendingIntent = PendingIntent.getActivity(
context,
0, // "Private request code for the sender", not used
Intent(context, activityClass).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
PendingIntent.FLAG_IMMUTABLE
)
val icon = if (isLocationDisabled) R.drawable.ic_notification_no_result else R.drawable.ic_notification // TODO design your icons
if (isLocationDisabled) {
val txt = """
Location is not currently enabled on your device. Without it, Bluetooth will not deliver beacon detections to the observer.
Tap to open the app
""".trimIndent()
notificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(txt))
}
return notificationBuilder
.setSmallIcon(icon)
.setContentTitle("App launched in background") // TODO set your title
.setContentText(if (isLocationDisabled) "Defect: geolocation is missing ..." else "Tap to open the app.")
.setContentIntent(pendingIntent)
.build()
}
}
IStatusCallback.kt
package my.package.ble
interface IStatusCallback {
fun onResult(isScanning: Boolean)
fun onError(message: String)
}
IEventListener.kt
package my.package.ble
interface IEventListener {
fun onEvent(line: String)
}