Defining the main application service

As you already know, our application is dealing with Notes and Todos. The current application implementation keeps our data locally stored in the SQLite database. This data will be synchronized with the backend instance running on some remote server. All operations related to the synchronization will be performed silently in the background of our application. All responsibility will be given to the service, which we will define now. Create a new package called service and a new class MainService that will extend the Android service class. Make sure your implementation looks like this:

    class MainService : Service(), DataSynchronization { 
 
      private val tag = "Main service" 
      private var binder = getServiceBinder() 
      private var executor = TaskExecutor.getInstance(1) 
 
      override fun onCreate() { 
        super.onCreate() 
        Log.v(tag, "[ ON CREATE ]") 
      } 
 
      override fun onStartCommand(intent: Intent?, flags: Int, startId:
Int): Int { Log.v(tag, "[ ON START COMMAND ]") synchronize() return Service.START_STICKY } override fun onBind(p0: Intent?): IBinder { Log.v(tag, "[ ON BIND ]") return binder } override fun onUnbind(intent: Intent?): Boolean { val result = super.onUnbind(intent) Log.v(tag, "[ ON UNBIND ]") return result } override fun onDestroy() { synchronize() super.onDestroy() Log.v(tag, "[ ON DESTROY ]") } override fun onLowMemory() { super.onLowMemory() Log.w(tag, "[ ON LOW MEMORY ]") } override fun synchronize() { executor.execute { Log.i(tag, "Synchronizing data [ START ]") // For now we will only simulate this operation! Thread.sleep(3000) Log.i(tag, "Synchronizing data [ END ]") } } private fun getServiceBinder(): MainServiceBinder =
MainServiceBinder() inner class MainServiceBinder : Binder() { fun getService(): MainService = this@MainService }
}

Let's explain our main service. As you already know, we will extend Android's Service class to get all service functionality. We also implemented the DataSynchronization interface that will describe the main functionality of our service, which is synchronization. Please refer to the following code:

    package com.journaler.service 
    interface DataSynchronization { 
      
     fun synchronize() 
    }

So, we defined the implementation for the synchronize() method that will actually simulate real synchronization. Later, we will update this code to perform real backend communication.

All important lifecycle methods are overridden. Pay attention to the bind() method! This method will return an instance of binder that is produced by calling the getServiceBinder() method. Thanks to the MainServiceBinder class, we will expose our service instance to the end user that will be able to trigger the synchronize mechanism whenever it is needed.

Synchronization is not triggered just by the end user, but also automatically by the service itself. We trigger synchronization when a service is started and when it is destroyed.

The next important point for us is the starting and stopping of MainService. Open your Journaler class that represents your application and apply this update:

     class Journaler : Application() { 
 
       companion object { 
         val tag = "Journaler" 
         var ctx: Context? = null 
       } 
 
       override fun onCreate() { 
         super.onCreate() 
         ctx = applicationContext 
         Log.v(tag, "[ ON CREATE ]") 
         startService() 
       } 
 
       override fun onLowMemory() { 
         super.onLowMemory() 
         Log.w(tag, "[ ON LOW MEMORY ]") 
         // If we get low on memory we will stop service if running. 
         stopService() 
       } 
 
       override fun onTrimMemory(level: Int) { 
         super.onTrimMemory(level) 
         Log.d(tag, "[ ON TRIM MEMORY ]: $level") 
       } 
 
       private fun startService() { 
         val serviceIntent = Intent(this, MainService::class.java) 
         startService(serviceIntent) 
       } 
 
       private fun stopService() { 
        val serviceIntent = Intent(this, MainService::class.java) 
        stopService(serviceIntent) 
       } 
 
     } 

When the Journaler application is created, MainService will be started. We will also add one small optimization. If it happens that our application gets low on memory, we will stop our MainService class. Since the service is started as sticky, if we explicitly kill our application, the service will restart.

So far, we covered the service starting and stopping and its implementation. As you probably remember our mockup, at the bottom of our application drawer, we planned to put one more item. We planned to have the synchronize button. Triggering this button would do synchronization with the backend.

We will add that menu item and connect it with our service. Let's do some preparation first. Open the NavigationDrawerItem class and update it as follows:

    data class NavigationDrawerItem( 
      val title: String, 
      val onClick: Runnable, 
      var enabled: Boolean = true 
    ) 

We introduced the enabled parameter. Like this, some of our application drawer items can be disabled if needed. Our synchronize button will be disabled by default and enabled when we bind to the main service. These changes must affect NavigationDrawerAdapter too. Please refer to the following code:

    class NavigationDrawerAdapter( 
      val ctx: Context, 
      val items: List<NavigationDrawerItem> 
      ) : BaseAdapter() { 
 
        private val tag = "Nav. drw. adptr." 
 
        override fun getView(position: Int, v: View?, group: 
ViewGroup?): View { ... val item = items[position] val title = view.findViewById<Button>(R.id.drawer_item) ... title.setOnClickListener { if (item.enabled) { item.onClick.run() } else { Log.w(tag, "Item is disabled: $item") } } return view } ...
}

Finally, we will update our MainActivity class as follows, so the synchronization button can trigger synchronization:

    class MainActivity : BaseActivity() { 
      ... 
      private var service: MainService? = null 
     
      private val synchronize: NavigationDrawerItem by lazy { 
        NavigationDrawerItem( 
          getString(R.string.synchronize), 
          Runnable { service?.synchronize() }, 
          false 
        ) 
     } 
 
     private val serviceConnection = object : ServiceConnection { 
        override fun onServiceDisconnected(p0: ComponentName?) { 
            service = null 
            synchronize.enabled = false 
        } 
 
        override fun onServiceConnected(p0: ComponentName?, binder: 
IBinder?) { if (binder is MainService.MainServiceBinder) { service = binder.getService() service?.let { synchronize.enabled = true } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val menuItems = mutableListOf<NavigationDrawerItem>() ... menuItems.add(synchronize) ... } override fun onResume() { super.onResume() val intent = Intent(this, MainService::class.java) bindService(intent, serviceConnection,
android.content.Context.BIND_AUTO_CREATE) } override fun onPause() { super.onPause() unbindService(serviceConnection) } ... }

We will bind or unbind the main service whether our main activity status is active or not. To perform binding, we need the ServiceConnection implementation as it will enable or disable the synchronization button depending on the binding state. Also, we will maintain the main service instance depending on the binding state. The synchronization button will have access to the service instance and trigger the synchronize() method when clicked.