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.
This is a companion discussion topic for the original entry at https://localazy.com/blog/floating-windows-on-android-2-foreground-service