Displaying an overlay window is another way that can be used to customize the look of an Android device. An app that uses an overlay window can display content on top of all other apps.
One example of usage are Facebook Messenger chat heads. Another example are the various apps that adjust brightness for a better reading experience.
Unfortunately, it also seems to be abused by some malware. Android tried to address the possible security issues. It added an additional permission screen in API level 23 where the user has to grant this permission, and in API level 26 the overlay window is displayed below critical system windows (like status bar).
I like this Android feature and I hope there won't be any more restrictions enforced by Google in the upcoming Android versions.
I used an overlay window in one of my apps - SnowChilling. The app displays falling snow animation on top of all other apps. It uses OpenGL ES2 with my custom particle system to render the snow.
This post is a quick overview of the stuff I learned while I was doing the necessary research, so that I could create my SnowChilling app.
You can find the sample code for this post on github.
Permissions
To use an overlay window inside of your app, your app requires the SYSTEM_ALERT_WINDOW. You need to add this permission to AndroidManifest.xml
<manifest ...>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application..
Additionally, starting from Android Marshmallow (API level 23), the user has to grant this permission explicitly. To check if we need to get additional permission from the user, you can do the following
// Check if we need to ask user to grant this permission explicitly
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
// We don't have this permission, we need to ask user to grant it
// ...
}
}
You might want to display some explanation to the user, before showing the settings section to grant the permission to draw over other apps to your app. If you then want to show the settings screen where the user can grant this permission
val settingsIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivityForResult(settingsIntent, PERMISSION_REQUEST_CODE)
PERMISSION_REQUEST_CODE
is a random integer I've assigned to the request to distinguish it from other requests.
You can then check if the user granted the permission in onActivityResult()
and report a warning if he didn't.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(this)) {
// Great! We have the permission.
// Proceed...
} else {
// Still no permission.
}
}
If the permission to draw over other apps is crucial to your app's functionality, and the user still didn't grant it, you might just want to show some explanation to user and exit your app.
Starting a Foreground Service That Manages The Overlay
Starting a foreground service might not be directly related to the overlay window functionality, but it was a crucial part of SnowChilling, so I want to mention it here.
Android Oreo enforced some additional limitations on background services. Because we want our overlay app to run even when the activity from which it is started is not foreground, I decided to use a foreground service to show the app overlay.
To create a foreground service, first update your AndroidManifest.xml
<manifest ...>
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application ...>
<service android:name=".OverlayService"/>
...
In the manifest file I added the declaration of our foreground service - OverlayService
.
As mentioned in the section above, the app requires SYSTEM_ALERT_WINDOW
permission for rendering our overlay over other apps.
Additionally, if you target Android Pie (API level 28) and higher, you also need to request the FOREGROUND_SERVICE permission for using a foreground service. It's a normal permission, so it's granted automatically during installation (you only need to have it in your AndroidManifest.xml).
I'm using OpenGL ES 2.0 for drawing the overlay, therefore I've also added the <uses-feature>
element and specified the OpenGL version, so that Google Play store can filter supported devices (most devices should support OpenGL ES 2.0).
The code for the service is the following
class OverlayService: Service() {
lateinit var rootView: View
lateinit var overlaySurfaceView: OverlaySurfaceView
lateinit var wm: WindowManager
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground()
return Service.START_STICKY
}
override fun onCreate() {
super.onCreate()
// Initialize the overlay here
initOverlay()
overlaySurfaceView.onResume()
}
override fun onDestroy() {
super.onDestroy()
// Stop rendering overlay
overlaySurfaceView.onPause()
wm.removeView(rootView)
}
fun startForeground() {
val pendingIntent: PendingIntent =
Intent(this, OverlayService::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, 0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE)
channel.lightColor = Color.BLUE
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.createNotificationChannel(channel)
}
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getText(R.string.app_name))
.setContentText(getText(R.string.app_name))
.setSmallIcon(R.drawable.notification_template_icon_bg)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.app_name))
.build()
startForeground(ONGOING_NOTIFICATION_ID, notification)
}
companion object {
val ONGOING_NOTIFICATION_ID = 6661
val CHANNEL_ID = "overlay_channel_id"
val CHANNEL_NAME = "overlay_channel_name"
}
}
I created a regular service and started it in foreground, so that it can continue to render our overlay even when the user leaves the activity that started the service.
The main configuration steps that make this service foreground are performed in my custom startForeground()
method.
I initialized the actual overlay view in onCreate()
method of the service and I stop rendering the overlay once onDestroy()
is called. You can use other events or lifecycle methods that better suit the purpose of your application.
For Android Oreo and higher you also need to create a NotificationChannel
, so I first create the channel and pass its ID into NotificationCompat.Builder()
. The channel ID argument is then ignored on Android versions lower that Android Oreo.
The code for the actual configuration of the overlay will be shown in the next section.
The service should be started by the user and run for arbitrary time until the user decides to stop it. The service is not really executing any tasks where it would make sense to stop the service once the task is processed. Therefore I decided to start the service with the START_STICKY constant.
You can then start the service somewhere in your Activity
val intent = Intent(this, OverlayService::class.java)
startService(intent)
Configuring Overlay Window
In the previous section I created a foreground services and in its onCreate()
method I called initOverlay()
to configure the overlay window. Here is the code for initializing the overlay
fun initOverlay() {
val li = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
rootView = li.inflate(R.layout.overlay, null)
overlaySurfaceView = rootView.findViewById(R.id.overlaySurfaceView)
val type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
else
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
val params = WindowManager.LayoutParams(
type,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ,
PixelFormat.TRANSLUCENT
)
wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.addView(rootView, params)
}
In the code above I first inflate a view with my xml layout.
I prepared some parameters for our overlay window. Before Android Oreo, I would use TYPE_SYSTEM_OVERLAY
. Starting from Android Oreo (API level 26) this flag got deprecated, so I used TYPE_APPLICATION_OVERLAY instead. This also means that on Oreo and higher, your app won't be able to draw over critical system windows (like the status bar).
I also wanted a format that supports transparency, therefore I used PixelFormat.TRANSLUCENT
.
You can additionally use the parameters to configure how to receive touch events. I just used FLAG_NOT_TOUCHABLE because I'm not handling any touch events. It's also useful to add FLAG_NOT_FOCUSABLE
, so that your app won't block some input events like the back button press.
The steps described above should be sufficient to create an app that draws your custom layout over other apps, but in SnowChilling I'm additionally using OpengGL to render the snow using my custom particle effects, so I decided to add a short section related to OpengGL. If you only want to use the regular UI elements without OpenGL, you can skip the next section.
Using OpenGL ES 2.0 With Overlay Window
Using OpenGL will give you the option to create GPU-accelerated effects when rendering your overlay app. In my SnowChilling app I'm using OpenGL ES 2.0 for GPU-accelerated particle effect and rendering.
I decided to quickly show how to draw a texture in this example because I'm also using a texture when rendering the snow in SnowChilling app. You have of course many other options what you can do with OpenGL and for more information I recommend to check out the information in official docs.
There are multiple ways how to set up OpengGL rendering on Android. For this example I decided to use a GLSurfaceView. This has the advantage that you don't need to set up the EGL stuff and you also don't need to manage the GL rendering thread. In SnowChilling app I'm configuring the EGL display and managing the GL rendering thread manually from C++ code, which makes things a bit more complicated.
GLSurfaceView
uses a Renderer to do the actual OpenGL rendering. The callbacks that you override are called from separate OpenGL thread and therefore you are safe to call OpenGL functions from these callbacks. The code to load and render a texture is the following
class OverlayRenderer(val context: Context): GLSurfaceView.Renderer {
private val mvpMatrix = FloatArray(16)
private val projectionMatrix = FloatArray(16)
private val viewMatrix = FloatArray(16)
private val vertexShaderCode =
"precision highp float;\n" +
"attribute vec3 vertexPosition;\n" +
"attribute vec2 uvs;\n" +
"varying vec2 varUvs;\n" +
"uniform mat4 mvp;\n" +
"\n" +
"void main()\n" +
"{\n" +
"\tvarUvs = uvs;\n" +
"\tgl_Position = mvp * vec4(vertexPosition, 1.0);\n" +
"}"
private val fragmentShaderCode =
"precision mediump float;\n" +
"\n" +
"varying vec2 varUvs;\n" +
"uniform sampler2D texSampler;\n" +
"\n" +
"void main()\n" +
"{\t\n" +
"\tgl_FragColor = texture2D(texSampler, varUvs);\n" +
"}"
private var vertices = floatArrayOf(
// x, y, z, u, v
-1.0f, -1.0f, 0.0f, 0f, 0f,
-1.0f, 1.0f, 0.0f, 0f, 1f,
1.0f, 1.0f, 0.0f, 1f, 1f,
1.0f, -1.0f, 0.0f, 1f, 0f
)
private var indices = intArrayOf(
2, 1, 0, 0, 3, 2
)
private var program: Int = 0
private var vertexHandle: Int = 0
private var bufferHandles = IntArray(2)
private var uvsHandle: Int = 0
private var mvpHandle: Int = 0
private var samplerHandle: Int = 0
private val textureHandle = IntArray(1)
var vertexBuffer: FloatBuffer = ByteBuffer.allocateDirect(vertices.size * 4).run {
order(ByteOrder.nativeOrder())
asFloatBuffer().apply {
put(vertices)
position(0)
}
}
var indexBuffer: IntBuffer = ByteBuffer.allocateDirect(indices.size * 4).run {
order(ByteOrder.nativeOrder())
asIntBuffer().apply {
put(indices)
position(0)
}
}
override fun onDrawFrame(p0: GL10?) {
// We want to clear buffers to transparent, so we also see stuff rendered by
// other apps and the system
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
GLES20.glClearColor(0f, 0f, 0f, 0f)
// Prepare transformations for our texture quad
// Position model
// ..
// Position camera
Matrix.setLookAtM(viewMatrix, 0, 0f, 0f, -80f, 0f, 0f, 0f, 0f, 1.0f, 0.0f)
// Combine all our transformations
Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
GLES20.glUseProgram(program)
// Pass transformations to shader
GLES20.glUniformMatrix4fv(mvpHandle, 1, false, mvpMatrix, 0)
// Prepare texture for drawing
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0])
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glUniform1i(samplerHandle, 0)
// Prepare buffers with vertices and indices
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferHandles[0])
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferHandles[1])
GLES20.glEnableVertexAttribArray(vertexHandle)
GLES20.glVertexAttribPointer(vertexHandle, 3, GLES20.GL_FLOAT, false, 4 * 5, 0)
GLES20.glEnableVertexAttribArray(uvsHandle)
GLES20.glVertexAttribPointer(uvsHandle, 2, GLES20.GL_FLOAT, false, 4 * 5, 3 * 4)
// Ready to draw
GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_INT, 0)
}
override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
// Set perspective projection
val apect: Float = width.toFloat() / height.toFloat()
Matrix.perspectiveM(projectionMatrix, 0, -apect, apect, 1f, 100f)
}
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
// Create shader program
val vertexShader: Int = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER).also { shader ->
GLES20.glShaderSource(shader, vertexShaderCode)
GLES20.glCompileShader(shader)
}
val fragmentShader: Int = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER).also { shader ->
GLES20.glShaderSource(shader, fragmentShaderCode)
GLES20.glCompileShader(shader)
}
program = GLES20.glCreateProgram().also {
GLES20.glAttachShader(it, vertexShader)
GLES20.glAttachShader(it, fragmentShader)
GLES20.glLinkProgram(it)
// Get handles
vertexHandle = GLES20.glGetAttribLocation(it, "vertexPosition")
uvsHandle = GLES20.glGetAttribLocation(it, "uvs")
mvpHandle = GLES20.glGetUniformLocation(it, "mvp")
samplerHandle = GLES20.glGetUniformLocation(it, "texSampler")
}
// Initialize buffers
GLES20.glGenBuffers(2, bufferHandles, 0)
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferHandles[0])
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertices.size * 4, vertexBuffer, GLES20.GL_DYNAMIC_DRAW)
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferHandles[1])
GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indices.size * 4, indexBuffer, GLES20.GL_DYNAMIC_DRAW)
// Load the texture
val bitmap = BitmapFactory.decodeStream(context.assets.open("bugjaeger_icon.png"))
GLES20.glGenTextures(1, textureHandle, 0)
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0])
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1)
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
glGenerateMipmap(GLES20.GL_TEXTURE_2D)
// Ensure I can draw transparent stuff that overlaps properly
GLES20.glEnable(GLES20.GL_BLEND)
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
}
}
In the code above I'm rendering a texture into a quad.
I load the texture from a bitmap stored in assets in the onSurfaceCreated()
callback. Here you can do all your OpenGL initialization.
In onSurfaceChanged()
I'm setting up some basic camera transformations because in this method I get the actual width and height of the surface as method arguments.
Rendering is performed in onDrawFrame()
. You can calculate your MVP matrix for moving you objects and camera between frames here and pass it into the shader.
You can than use our custom Renderer
together with the GLSurfaceView
class OverlaySurfaceView : GLSurfaceView {
var overlayRenderer: OverlayRenderer
constructor(context: Context?, attrs: AttributeSet) : super(context, attrs) {
// Configure OpenGL ES 2.0 and enable rendering with transparent background
setEGLContextClientVersion(2)
setEGLConfigChooser(8, 8, 8, 8, 16, 0)
getHolder().setFormat(PixelFormat.RGBA_8888)
setZOrderOnTop(false)
overlayRenderer = OverlayRenderer(context!!)
setRenderer(overlayRenderer)
}
}
I tried to configure OverlaySurfaceView
, so that other apps and system UI are still visible.
Now you can use your custom GLSurfaceView
in your xml layout
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
android:layout_height="match_parent">
<eu.sisik.overlay.OverlaySurfaceView
android:id="@+id/overlaySurfaceView"
android:background="@android:color/transparent"
android:layout_width="0dp"
android:layout_height="0dp" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
In the github repo I added some additional behavior to the example to make it a bit less boring. The icon from my Bugjaeger app is trying to catch bugs. Here's how it looks
More Useful Resources
A background service can also be used to apply effects to a video with OpengGL even without requiring the additional permissions and overlay window. I wrote another blogpost which shows how to generate a video file from captured images using the MediaCodec API
inside of a background IntentService
.
Additionally, I also recommend to check out my other blogpost that shows how to process images efficiently with C/C++ and RenderScript.