zakopane

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)
            }

        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"
    }
}

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.

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,
        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.

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

bugjaeger_hunting_bug

Previous Post

Add a comment