DEV Community

Cover image for #2 Floating Windows on Android: Foreground Service
vaclavhodek for Localazy

Posted on • Edited on

#2 Floating Windows on Android: Foreground Service

Have you ever wondered how to make those floating windows used by Facebook Heads and other apps? Have you ever wanted to use the same technology in your app? It’s easy, and I will guide you through the whole process.

I'm the author of Floating Apps; the first app of its kind on Google Play and the most popular one with over 8 million downloads. After 6 years of the development of the app, I know a bit about it. It’s sometimes tricky, and I spent months reading documentation and Android source code and experimenting. I received feedback from tens of thousands of users and see various issues on different phones with different Android versions.

Here's what I learned along the way.

Before reading this article, it's recommended to go through Floating Windows on Android 1: Jetpack Compose & Room.

In this article, I teach you how to build the long-running foreground service that is necessary for floating windows and what are the limitations.

The Service

For the floating technology, it's necessary to have Service and not Activity. Android can have only one Activity in the foreground and so if we use Activity, other apps would be paused or restarted. And that's not the desired behavior - we want not to interrupt the current task in any way.

Standard Android services are not designed for long-running operations. They are rather designed to do a task in the background and finish.

To avoid our Service from being killed by the Android system, it's better to use a foreground service.

For our specific simple app, we use a service that is always running and renders a permanent notification. For your app, having the service running only when there are some floating windows active may be a better approach.

The magic is hidden in the overriding onStartCommand method and returning START_STICKY and START_NOT_STICKY correctly. The source code for this is shown below in this article.

The Limitations

Show Notification

A foreground service must show permanent/foreground notification shortly after the service is launched. If we fail to do so, the app is terminated.

On some devices, this may cause occasional crashes as the process may take a bit longer than the hard-coded interval.

Be sure to show the foreground notification as the first thing. The source code for this is shown below in this article.

Also, some users simply dislike the permanent notification being shown, but there is a little we can do about it. They can, on some devices, hide the notification in the phone’s Settings.

Killed On Some Devices

On some phones and tablets, it's impossible to avoid the services from being killed thanks to the vendors who are integrating aggressive memory and process management.

There is an excellent website on this topic: Don't kill my app!

The Permission

From Android API level 28, extra permission is necessary for foreground services. Add the line below to your AndroidManifest.xml:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

The Notification

From Android O, a permanent notification is required, and with all the experience, I would recommend to use it for older versions too to prevent the service from being killed by Android.

The full source code for the foreground notification, including the code for stopping the service:

/**  
 * Remove the foreground notification and stop the service. 
 */
private fun stopService() {  
  stopForeground(true)  
  stopSelf()  
}

/**
 * Create and show the foreground notification.
 */
private fun showNotification() {

  val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

  val exitIntent = Intent(this, FloatingService::class.java).apply {
    putExtra(INTENT_COMMAND, INTENT_COMMAND_EXIT)
  }

  val noteIntent = Intent(this, FloatingService::class.java).apply {
    putExtra(INTENT_COMMAND, INTENT_COMMAND_NOTE)
  }

  val exitPendingIntent = PendingIntent.getService(
    this, CODE_EXIT_INTENT, exitIntent, 0
  )

  val notePendingIntent = PendingIntent.getService(
    this, CODE_NOTE_INTENT, noteIntent, 0
  )

  // From Android O, it's necessary to create a notification channel first.
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    try {
      with(
        NotificationChannel(
          NOTIFICATION_CHANNEL_GENERAL,
          getString(R.string.notification_channel_general),
          NotificationManager.IMPORTANCE_DEFAULT
        )
      ) {
        enableLights(false)
        setShowBadge(false)
        enableVibration(false)
        setSound(null, null)
        lockscreenVisibility = Notification.VISIBILITY_PUBLIC
        manager.createNotificationChannel(this)
      }
    } catch (ignored: Exception) {
      // Ignore exception.
    }
  }

  with(
    NotificationCompat.Builder(
      this,
      NOTIFICATION_CHANNEL_GENERAL
    )
  ) {
    setTicker(null)
    setContentTitle(getString(R.string.app_name))
    setContentText(getString(R.string.notification_text))
    setAutoCancel(false)
    setOngoing(true)
    setWhen(System.currentTimeMillis())
    setSmallIcon(R.drawable.ic_launcher_foreground)
    priority = Notification.PRIORITY_DEFAULT
    setContentIntent(notePendingIntent)
    addAction(
      NotificationCompat.Action(
        0,
        getString(R.string.notification_exit),
        exitPendingIntent
      )
    )
    startForeground(CODE_FOREGROUND_SERVICE, build())
  }

}

Our notification is permanent and cannot be cancelled. It's clickable and when clicked, it invokes INTENT_COMMAND_NOTE command. Also, the notification has the exit action to invoke INTENT_COMMAND_EXIT.

OnStartCommand

As mentioned above, the magic behavior of the service is hidden inside onStartCommand. It's simple:

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

  val command = intent.getStringExtra(INTENT_COMMAND) ?: ""

  // Exit the service if we receive the EXIT command.
  // START_NOT_STICKY is important here, we don't want 
  // the service to be relaunched. 
  if (command == INTENT_COMMAND_EXIT) {
    stopService()
    return START_NOT_STICKY
  }

  // Be sure to show the notification first for all commands.
  // Don't worry, repeated calls have no effects.
  showNotification()

  // Show the floating window for adding a new note.
  if (command == INTENT_COMMAND_NOTE) {
    Toast.makeText(
      this, 
      "Floating window to be added in the next lessons.", 
      Toast.LENGTH_SHORT
    ).show()
  }

  return START_STICKY
}

Service & AndroidManifest

For our service, we need a record in AndroidManifest.xml file.

<service  
  android:name=".FloatingService"  
  android:excludeFromRecents="true"  
  android:exported="false"  
  android:label="@string/app_name"  
  android:roundIcon="@mipmap/ic_launcher_round"
  android:stopWithTask="false" />

Explanation of parameters above:

  • android:excludeFromRecents - Don't show the service in Recent items screen.
  • android:exported - There is no reason for the service to be accessible from outside the app.
  • android:stopWithTask - Don't stop the service when the app is terminated, e.g. swiped out of the Recent items screen.

Start Service

For starting the service, let's create a small helper method. We need to handle the requirement for Android O and above - to use startForegroundService instead of startService.

fun Context.startFloatingService(command: String = "") {  

  val intent = Intent(this, FloatingService::class.java)  
  if (command.isNotBlank()) {
    intent.putExtra(INTENT_COMMAND, command)  
  }

  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {  
    this.startForegroundService(intent)  
  } else {  
    this.startService(intent)  
  }  

}

In one of the following articles, we will learn how to start our service when the device boot, but for the time being, let's stick with starting the service when the main app is launched. For this, we just add a single line to our existing MainActivity's onCreate method.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  // Start the foreground service.
  startFloatingService()

  // ... the rest of the code ...

}

Multi Process Approach

If your floating service is heavy and may cause occasional crashes, you may want to separate it from your app.

You can use android:multiprocess in your AndroidManifest.xml and separate your activities from the service by running them in a different process.

However, keep in mind that using more processes involves extra effort for synchronizing state as activities, and the service would no longer share memory.

Localization

Above, we added new strings notification_channel_general, notification_text and notification_exit, so be sure to run the Gradle task uploadStrings to upload your translations to the Localazy platform for translating.

A minute after I uploaded my 11 strings, 6 of them are ready in 80 languages!

More information on the importance of app localization was described in Floating Windows on Android 1: Jetpack Compose & Room.

Results

The animation below demonstrates how the permanent notification is shown when the main app is launched and remains active even if the main app is removed from the Recent items screen.

Source Code

The whole source code for this article is available on Github.

Stay Tuned

Eager to learn more about Android development? Follow me (@vaclavhodek) and Localazy (@localazy) on Twitter, or like Localazy on Facebook.

The Series

This article is part of the Floating Windows on Android series.

Top comments (0)