Skip to content

Instantly share code, notes, and snippets.

@handstandsam
Last active February 16, 2024 19:42
  • Star 58 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save handstandsam/6ecff2f39da72c0b38c07aa80bbb5a2f to your computer and use it in GitHub Desktop.
Jetpack Compose OverlayService. You have to have all the correct permissions granted and in your manifest, but if you do, this this will show a green box with "Hello" in it!
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
internal class MyLifecycleOwner : SavedStateRegistryOwner {
private var mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private var mSavedStateRegistryController: SavedStateRegistryController = SavedStateRegistryController.create(this)
/**
* @return True if the Lifecycle has been initialized.
*/
val isInitialized: Boolean
get() = true
override fun getLifecycle(): Lifecycle {
return mLifecycleRegistry
}
fun setCurrentState(state: Lifecycle.State) {
mLifecycleRegistry.currentState = state
}
fun handleLifecycleEvent(event: Lifecycle.Event) {
mLifecycleRegistry.handleLifecycleEvent(event)
}
override fun getSavedStateRegistry(): SavedStateRegistry {
return mSavedStateRegistryController.savedStateRegistry
}
fun performRestore(savedState: Bundle?) {
mSavedStateRegistryController.performRestore(savedState)
}
fun performSave(outBundle: Bundle) {
mSavedStateRegistryController.performSave(outBundle)
}
}
import android.app.AlertDialog
import android.app.Service
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.util.TypedValue
import android.view.Window
import android.view.WindowManager
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.lifecycle.ViewTreeViewModelStoreOwner
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
import com.viatek.fitnation.echelon_android.R
class OverlayService : Service() {
val windowManager get() = getSystemService(WINDOW_SERVICE) as WindowManager
override fun onCreate() {
super.onCreate()
setTheme(R.style.ThemeOverlay_AppCompat_Light)
showOverlay()
}
private fun showOverlay() {
val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
layoutFlag,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)
val composeView = ComposeView(this)
composeView.setContent {
Text(
text = "Hello",
color = Color.Black,
fontSize = 50.sp,
modifier = Modifier
.wrapContentSize()
.background(Color.Green)
)
}
// Trick The ComposeView into thinking we are tracking lifecycle
val viewModelStore = ViewModelStore()
val lifecycleOwner = MyLifecycleOwner()
lifecycleOwner.performRestore(null)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
ViewTreeLifecycleOwner.set(composeView, lifecycleOwner)
ViewTreeViewModelStoreOwner.set(composeView) { viewModelStore }
ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)
windowManager.addView(composeView, params)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
}
@wilinz
Copy link

wilinz commented Jun 17, 2022

The UI doesn't restructure when my mutableState changes, how to deal with this?

@KONFeature
Copy link

I've reviewed a bit you're implementation to be more generic, with a dedicated ui context (needed to access window manger from a background context for API 12) and using the lifecycle of the service, you can check it out here : https://gist.github.com/KONFeature/2f84436e1c0a1926505cac934d470f90

@zoltish
Copy link

zoltish commented Jun 29, 2022

@KONFeature That looks nice, although Im curious about the dedicated ui context. I dont use that in my solution, yet it works perfectly fine on Android 12. Why would that be? Im just using the services (this) context.

@KONFeature
Copy link

@zoltish It will work, but not really perfectly, if you enable strict mode logging you will see that the system isn't happy to have a non UI context as context when building the view. The fact that it's a background thread will prevent the component (composable, window manager, layout inflater etc), to access UI constants ressources.

Extract of the strict mode error log with non ui context when building the ComposeView :

E/ViewConfiguration: Tried to access UI constants from a non-visual Context:...VideoCallOverlayService@8a2596aUI constants, such as display metrics or window metrics, must be accessed from Activity or other visual Context. Use an Activity or a Context created with Context#createWindowContext(int, Bundle), which are adjusted to the configuration and visual bounds of an area on screen
    java.lang.IllegalArgumentException: Tried to access UI constants from a non-visual Context:...VideoCallOverlayService@8a2596a
        at android.view.ViewConfiguration.get(ViewConfiguration.java:510)
        at android.view.View.<init>(View.java:5317)
        at android.view.View.<init>(View.java:5467)
        at android.view.ViewGroup.<init>(ViewGroup.java:697)
        at android.view.ViewGroup.<init>(ViewGroup.java:693)
        at android.view.ViewGroup.<init>(ViewGroup.java:689)
        at android.view.ViewGroup.<init>(ViewGroup.java:685)
        at androidx.compose.ui.platform.AndroidComposeView.<init>(AndroidComposeView.android.kt:154)

You will get this log for the composable view creation, and and you will have a similar one for the window manager :)

@zoltish
Copy link

zoltish commented Jun 30, 2022

@KONFeature Interesting! Thanks for letting me know about it :) Noteworthy: createWindowContext is available on R and above.

@reconman
Copy link

reconman commented Jul 1, 2022

Since lifecycle-viewmodel-savedstate 2.5.0, the line

ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)

needs to be changed to

composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)

They did some Kotlin refactoring.

@aabadaa
Copy link

aabadaa commented Aug 14, 2022

In newer libraries versions , we have to replace
ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)
by
composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)

@NamekMaster
Copy link

That's working for me! But the shadow is missing while I show my composable. I've tried adding extra padding in the parent of my composable, or in ComposeView. The shadow works well when I put them into activities. Could anyone help me?
Here is my code:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Overlay(
    modifier: Modifier = Modifier,
    onDrag: (offsetX: Float, offsetY: Float) -> Unit,
    onClick: () -> Unit,
) {
    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }
    val drag = Modifier.pointerInput(Unit) {
        detectDragGestures(onDragStart = {}, onDrag = { change, dragAmount ->
            change.consume()
            offsetX += dragAmount.x
            offsetY += dragAmount.y
            onDrag(offsetX, offsetY)
        }, onDragEnd = {})
    }
    Box(
        Modifier
            .background(Color.White)
            .zIndex(0f)
            .padding(50.dp)
    ) {
        Surface(
            elevation = 10.dp,
            shape = CircleShape,
            onClick = onClick,
            modifier = modifier.then(drag),
        ) {
            Image(
                painter = painterResource(id = R.drawable.avatar_green),
                contentDescription = "assistant",
            )
        }
    }
}

@realskyrin
Copy link

Nice work !!!

@mairs8
Copy link

mairs8 commented Oct 30, 2022

I am getting error: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@6c83604 -- permission denied for window type 2038.

Does anyone know how to fix?

@FreePhoenix888
Copy link

@mairs8 Add <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> to your manifest

@FreePhoenix888
Copy link

The UI doesn't restructure when my mutableState changes, how to deal with this?

@wilinz I have the same same problem. Have you solved it?

@wilinz
Copy link

wilinz commented Nov 18, 2022

The UI doesn't restructure when my mutableState changes, how to deal with this?

@wilinz I have the same same problem. Have you solved it?

not

@FreePhoenix888
Copy link

@wilinz , I have found the solution and created the question and answer on stackoverflow to help others that cannot find this info: https://stackoverflow.com/questions/74433762/how-to-show-a-view-when-an-android-app-is-not-active-focused

@FreePhoenix888
Copy link

I am getting error: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@6c83604 -- permission denied for window type 2038.

Does anyone know how to fix?
@mairs8
You have to add the permission to use Window Manager
https://stackoverflow.com/questions/74433762/how-to-show-a-view-when-an-android-app-is-not-active-focused

@FreePhoenix888
Copy link

That's working for me! But the shadow is missing while I show my composable. I've tried adding extra padding in the parent of my composable, or in ComposeView. The shadow works well when I put them into activities. Could anyone help me? Here is my code:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Overlay(
    modifier: Modifier = Modifier,
    onDrag: (offsetX: Float, offsetY: Float) -> Unit,
    onClick: () -> Unit,
) {
    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }
    val drag = Modifier.pointerInput(Unit) {
        detectDragGestures(onDragStart = {}, onDrag = { change, dragAmount ->
            change.consume()
            offsetX += dragAmount.x
            offsetY += dragAmount.y
            onDrag(offsetX, offsetY)
        }, onDragEnd = {})
    }
    Box(
        Modifier
            .background(Color.White)
            .zIndex(0f)
            .padding(50.dp)
    ) {
        Surface(
            elevation = 10.dp,
            shape = CircleShape,
            onClick = onClick,
            modifier = modifier.then(drag),
        ) {
            Image(
                painter = painterResource(id = R.drawable.avatar_green),
                contentDescription = "assistant",
            )
        }
    }
}

@NamekMaster Have you tried to add recomposer? https://stackoverflow.com/questions/74433762/how-to-show-a-view-when-an-android-app-is-not-active-focused

@zenyagami
Copy link

Amazing works great!!

@tungthanhss
Copy link

    ViewTreeLifecycleOwner.set(composeView, lifecycleOwner)
    ViewTreeViewModelStoreOwner.set(composeView) { viewModelStore }
    ViewTreeSavedStateRegistryOwner.set(composeView, lifecycleOwner)

I can not import this by lifecycle version 2.6.1. Anyone fix it ?

@tberghuis
Copy link

@tungthanhss the api changed to extension functions on View

composeView.setViewTreeLifecycleOwner(lifecycleOwner)
composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
composeView.setViewTreeViewModelStoreOwner(viewModelStoreOwner)

@tungthanhss
Copy link

tungthanhss commented Mar 26, 2023

viewModelStoreOwner

thank you but how i can get viewModelStoreOwner
val viewModelStoreOwner = ViewModelStoreOwner { viewModelStore } not working now

@tberghuis
Copy link

val viewModelStore = ViewModelStore()
val viewModelStoreOwner = object : ViewModelStoreOwner {
  override val viewModelStore: ViewModelStore
    get() = viewModelStore
}

@reconman
Copy link

reconman commented Apr 19, 2023

Does anyone else get the error Class 'MyLifecycleOwner' is not abstract and does not implement abstract member public abstract val lifecycle: Lifecycle defined in androidx.savedstate.SavedStateRegistryOwner with lifecycle version 2.6.1?

@samuelpena21
Copy link

Does anyone else get the error Class 'MyLifecycleOwner' is not abstract and does not implement abstract member public abstract val lifecycle: Lifecycle defined in androidx.savedstate.SavedStateRegistryOwner with lifecycle version 2.6.1?

I'm having the same issue

@AhmedMousa7
Copy link

Does anyone else get the error Class 'MyLifecycleOwner' is not abstract and does not implement abstract member public abstract val lifecycle: Lifecycle defined in androidx.savedstate.SavedStateRegistryOwner with lifecycle version 2.6.1?

I am facing the same problem with version 2.5.1. However, I have managed to resolve it by overriding the lifecycle as a variable instead of a function. Although the IDE is currently displaying an error ("lifecycle" overrides nothing), but the project can still be compiled and built without any issues.

override val lifecycle: Lifecycle get() = lifecycleRegistry

@theGBguy
Copy link

theGBguy commented May 2, 2023

I am using this solution and it's been working fine. The only issue I got is overlay is kind of transparent no matter what I do. The content behind the overlay is visible as shown in the screenshot attached and also the button is not clickable. Any help would be appreciated.

Screenshot_20230502_114514

@theGBguy
Copy link

theGBguy commented May 3, 2023

I have found a fix to the problem. Removing FLAG_NOT_TOUCHABLE and setting alpha of params to 1f did the job. Now the button or any other views/composables are receiving click events and also the transparency is removed. Also, Thanks to @tberghuis for his excellent Floating Timer open-source code.

@SageJustus
Copy link

SageJustus commented May 18, 2023

Does anyone else get the error Class 'MyLifecycleOwner' is not abstract and does not implement abstract member public abstract val lifecycle: Lifecycle defined in androidx.savedstate.SavedStateRegistryOwner with lifecycle version 2.6.1?

I am facing the same problem with version 2.5.1. However, I have managed to resolve it by overriding the lifecycle as a variable instead of a function. Although the IDE is currently displaying an error ("lifecycle" overrides nothing), but the project can still be compiled and built without any issues.

override val lifecycle: Lifecycle get() = lifecycleRegistry

I also ran into this problem. Follow @AhmedMousa7 's method to solve this problem,

Android Studio prompts

Class 'LifecycleOwner' is not abstract and does not implement abstract member public abstract fun getLifecycle(): Lifecycle defined in androidx.savedstate.SavedStateRegistryOwner
'lifecycle' overrides nothing

but gradle compiles and runs without problems.

I found that the version of LifecycleOwner inherited by SavedStateRegistryOwner is wrong. The dependency it uses is androidx.lifecycle:lifecycle-common:2.0.0, not the expected 2.6.1. I suspect this is the reason.

Anyone else also using androidx.lifecycle:lifecycle-common:2.0.0?

@KuhakuPixel
Copy link

KuhakuPixel commented Jul 27, 2023

There is an open source library to create overlay view with jetpackcompose
https://github.com/KuhakuPixel/UberAlles/

(DISCLAIMER: I am the author and still in early stage of development, would appreciate your feedback and fix :D )

@philip-segerfast
Copy link

@wilinz @FreePhoenix888 I fixed the ComposeView not recomposing by, in addition to lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE), adding

lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)

@xrubioj
Copy link

xrubioj commented Oct 16, 2023

Reached the same conclusions on some of the comments, specially the updated APIs and the requirement to trigger the START and RESUME lifecycle events so recomposition worked. I found the snippet here, that's why I was unable to find the updates suggested by commenters: https://www.jetpackcompose.app/snippets/OverlayService See my code here: https://github.com/xrubioj/JetpackComposeOverlayTest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment