By: Hryhorii Kirochkin
19 JAN 2021 697
Often, applications created for small and medium businesses are designed to simplify the use of the services provided by the company.
This is an interactive catalog of goods, appointment-making functionality and other possible features. However, imagine that a customer, entering a store, automatically sends a notification to the server that he or she has arrived, and the manager or administrator, already knowing what the client looks like, his name and preferences, comes with a personalized offer. Sounds futuristic? We don’t think so, iBeacon technology allows us to do this and this feature is implemented in our product “Barbershop”.
Next question, which naturally arises - how can this be implemented? Let’s dig deeper.
What is iBeacon?
In simple words, iBeacon allows you to create a connection between devices of a client and a manager using Bluetooth connection.
Also, you can put a special device-beacon that can track the movement of customers in the store, create a map of movements, show where customers spent more time, and what products were not looked at at all, analyse what products they were choosing from. The possibilities are endless. However, in our case, the beacon will be the phone number of the manager who sends the signal, and the customer's phone will receive it when entering the store and send a signal to the server that it is close to the manager, and the manager will see who came to the store.
There are some open-source packages that allow you to do this, we will not focus on specific implementation. The problem with all these packages is that this whole system works only if the application is open either in the background (considered that the smartphone OS does not put it in a sleep mode). But it is necessary that it is always working. So how do we force technology to work even with the turned off application? We are going to cover it in this article.
Beginning of work
So that we can read the data even with a turned off application, we need to create a permanent notification, bind a service to it, that will do something in the background (in our case, wait for a beacon signal). For simplicity, all examples of native code will be presented for Android, to do it in iOS is not hard. In addition, although the technology was originally written for iOS, it works great on Android, and there is no big difference. You can read in our blog about the differences and comparisons of the security of the two systems.
So, create an empty Flutter project with a counter of clicks and button for iterations. After that we start modification of the native part of the project:
1. Create
ForegroundService.kt, which we will manage the notification:
class ForegroundService : Service() {
private lateinit var notificationCreator: ForegroundServiceNotificationCreator
private val timer = Timer()
override fun onBind(intent: Intent?): IBinder = throw UnsupportedOperationException()
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
when (intent.action) {
ACTION_START_FOREGROUND -> startService()
ACTION_STOP_FOREGROUND -> stopService()
ACTION_CALL_FLUTTER -> getToFlutter()
}
return super.onStartCommand(intent, flags, startId)
}
private fun startService() {
notificationCreator = com.appus.barbershop.ForegroundServiceNotificationCreator(applicationContext)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR) {
startForeground(SERVICE_ID, notificationCreator.createNotification())
}
}
private fun getToFlutter(){
}
private fun stopService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR) {
stopForeground(true)
}
stopSelf()
}
companion object {
private const val SERVICE_ID = 1
const val ACTION_START_FOREGROUND = "ACTION_START_FOREGROUND"
const val ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND"
const val ACTION_CALL_FLUTTER = "ACTION_CALL_FLUTTER"
}
}
2.Create a helper class ForegroundServiceNotificationCreator.kt:
class ForegroundServiceNotificationCreator(private val context: Context) {
fun createNotification(): Notification {
setNotificationChannel(context)
return getNotification(context, getStopServiceIntent(context), getFlutterIntent(context))
}
private fun setNotificationChannel(context: Context) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val priority = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(
CHANNEL_FOREGROUND_NAME,
CHANNEL_NAME,
priority
)
notificationManager.createNotificationChannel(channel)
}
}
private fun getNotification(context: Context, stopIntent: PendingIntent?, runFLutterIntent : PendingIntent?): Notification {
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_FOREGROUND_NAME)
.setSmallIcon(android.R.drawable.ic_menu_more)
.setContentTitle(context.getString(R.string.service_name))
.setOngoing(true)
stopIntent?.let { it ->
notificationBuilder.addAction(
android.R.drawable.ic_menu_close_clear_cancel,
context.getString(R.string.stop_service), it
)
}
runFLutterIntent?.let { it ->
notificationBuilder.addAction(
android.R.drawable.ic_menu_close_clear_cancel,
context.getString(R.string.flutter_call), it
)
}
return notificationBuilder.build()
}
private fun getStopServiceIntent(context: Context): PendingIntent {
val stopServiceIntent = Intent(context, ForegroundService::class.java)
stopServiceIntent.action =
ForegroundService.ACTION_STOP_FOREGROUND
return PendingIntent.getService(
context,
REQUEST_CODE,
stopServiceIntent,
FLAGS
)
}
private fun getFlutterIntent(context: Context): PendingIntent {
val stopServiceIntent = Intent(context, ForegroundService::class.java)
stopServiceIntent.action =
ForegroundService.ACTION_CALL_FLUTTER
return PendingIntent.getService(
context,
REQUEST_CODE,
stopServiceIntent,
FLAGS
)
}
companion object {
private const val CHANNEL_FOREGROUND_NAME = "com.example.beacon"
private const val CHANNEL_NAME = "beacon"
private const val REQUEST_CODE = 0
private const val FLAGS = 0
}}
As you can see, a platform for calling Flutter from the native code (function getToFlutter in file ForegroundService.kt).
3. Editing MainActivity.kt so that you can send a signal from Flutter to start the service:
class MainActivity : FlutterActivity(){
private val CHANNEL = "com.appus.barbershop/beacon_service"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
// Note: this method is invoked on the main thread.
call, result ->
if (call.method == "startService") {
onStartServiceClick()
} else {
result.notImplemented()
}
}
}
fun onStartServiceClick() {
val intent = Intent(applicationContext, ForegroundService::class.java)
intent.action = ForegroundService.ACTION_START_FOREGROUND
startService(intent)
}
fun onStopServiceClick() {
val intent = Intent(applicationContext, ForegroundService::class.java)
intent.action = ForegroundService.ACTION_STOP_FOREGROUND
startService(intent)
}
}
You can see that the service is initiated by using the channel, which will be called from our Flutter code to start the service.
At the moment, we can temporarily suspend work on the native part and go to Flutter parts of the application. We will write a function for the call from the service and call the channel to start it. Here's what is needed to do:
1.Write a button handler to call channel launching service:
Future startService() async {
const platform = const MethodChannel('com.appus.barbershop/beacon_service');
try {
await platform.invokeMethod('startService');
} on PlatformException catch (e) {
print(e);
}
}
2. Write a function that will be called by the service:
void backgroundExecute() {
print('Hello world');
}
Attention! This function should be in the file main.dart in the global visibility.
Now we are ready to implement the service itself.
Let's get to this part:
In Flutter it is possible to transfer data to the native code and back using such a thing as channel.
Channels can not only call functions or show a separate screen, written entirely natively, but transfer data there. However, since, according to the problem statement, it is necessary to implement monitoring even when the application is closed, and through channels it is possible to transmit information only while MainActivity is working.
In our case MainActivity ends with the application, and there is no way to transfer data back, besides, data in fact there is nowhere to transfer because there is no Flutter. But what should we do in this case?
That's why we wrote the function backgroundExecute - you can trigger without the main activity, and even without Flutter.
The following screenshot shows how to call this function from a service, when the application itself is already closed.
private fun getToFlutter(){
val sBackgroundFlutterView = FlutterNativeView(applicationContext, true)
val args = FlutterRunArguments()
args.bundlePath = FlutterMain.findAppBundlePath(applicationContext)
args.entrypoint = "backgroundExecute"
sBackgroundFlutterView.runFromBundle(args)
}
So you call the function Flutter from a service with a notification, but not having MainActivity.
How can we transfer to Flutter using this method? After several unsuccessful attempts and searches in the documentation this issue was found on Github:
https://github.com/flutter/flutter/issues/15798
This way, you can warn the manager that someone has come to the store, but there is no way to transfer information about who exactly came. However, in our application Barbershop we found a way to transfer information about a specific visitor.