Search code examples
androidkotlinandroid-jetpack-composeoverlay

Removing Overlay on Back Button Press in Android Composable


I'm encountering an issue with my Overlay service in Android composed using Jetpack Compose. I'm trying to remove the overlay whenever the back button is pressed, but I'm facing difficulties implementing this functionality. Specifically, when I attempted to use BackHandler, I encountered the error message "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner".

Here's the service:

class OverlayService : Service() {

val windowManager get() = getSystemService(WINDOW_SERVICE) as WindowManager

override fun onCreate() {
    super.onCreate()
    this.updateAppLanguage()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    // Handle the incoming intent
    handleIntent(intent)
    return super.onStartCommand(intent, flags, startId)
}


private fun handleIntent(intent: Intent?) {
    // Check if the intent is not null
    intent?.let {
        // Extract data from the intent
        val number = it.getStringExtra("number")
        val dateEnd = it.getLongExtra("date", 0)

        if (number != null) {
            showOverlay(number, dateEnd)
        }

    }
}

private fun showOverlay(number: String, date: Long) {
    val layoutFlag: Int =
        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

    val params = WindowManager.LayoutParams(
        WindowManager.LayoutParams.MATCH_PARENT,
        WindowManager.LayoutParams.MATCH_PARENT,
        layoutFlag,
        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        PixelFormat.TRANSLUCENT
    )

    PreviousCallCard.getTrackerInfo(this)



    val composeView = ComposeView(this)

    composeView.setContent {

        val context = LocalContext.current
        val rememberAds by remember {
            mutableStateOf(PreviousCallCard.showAd(context))
        }
        val animVisibleState = remember {
            MutableTransitionState(false)
                .apply { targetState = true }
        }
        if (!animVisibleState.targetState &&
            !animVisibleState.currentState
        ) {
            PreviousCallCard.finishedShowingCard(this)
            windowManager.removeView(composeView)
            [email protected]()

        }


        AnimatedVisibility(
            modifier = Modifier,
            visibleState = animVisibleState,
            enter = fadeIn(),
            exit = fadeOut()
        ) {

            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Black.copy(alpha = 0.5f))
                    .clickable(
                        interactionSource = remember { MutableInteractionSource() },
                        indication = null
                    ) {
                        animVisibleState.targetState = false
                    },
                verticalArrangement = Arrangement.Bottom

            ) {

                PreviousCallCard(
                    modifier = Modifier,
                    number = number,
                    endDate = date,
                    onWhatsAppClick = {
                        val whatsappPackage = "com.whatsapp"
                        val packageManager = context.packageManager
                        val isWhatsAppInstalled = try {
                            packageManager.getPackageInfo(
                                whatsappPackage,
                                PackageManager.GET_ACTIVITIES
                            )
                            true
                        } catch (e: PackageManager.NameNotFoundException) {
                            false
                        }

                        if (!isWhatsAppInstalled) {
                            val whatsappUrl =
                                "https://play.google.com/store/apps/details?id=$whatsappPackage"
                            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(whatsappUrl))
                            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                            context.startActivity((intent))


                        } else {
                            val whatsappVideoId = getIdWhatsAppVideo(contentResolver, it)
                            if (whatsappVideoId != null) {
                                launchWhatsAppIntent(
                                    context,
                                    launcher = null,
                                    whatsappVideoId,
                                    true
                                )
                            } else {
                                launchWhatsAppMessageIntent(
                                    context = context,
                                    isWhatsAppInstalled = true,
                                    launcher = null,
                                    phoneNumber = number
                                )
                            }
                        }
                        animVisibleState.targetState = false

                    },
                    onViewProfile = {
                        context.startActivity(MainActivity.getStartIntent(context, ExtraDataType.VIEW_PROFILE))
                        animVisibleState.targetState = false
                    },
                    share = { name ->

                        if (name == "Unknown") {
                            launchLogShareIntent(
                                context,
                                number,
                                true
                            )

                        } else {
                            val lookUpContact =
                                getLookUpKey(contentResolver = contentResolver, name = name)
                            if (lookUpContact != null) {
                                launchContactShareIntent(context, lookUpContact, true)
                            }
                        }
                        animVisibleState.targetState = false


                    },
                    onClose = {
                        animVisibleState.targetState = false

                    },
                    navigateToCall = {

                    }
                )

                Spacer(modifier = Modifier.size(5.dp))

                if (rememberAds == AdsType.BANNER_ID) {
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(Color.White, RoundedCornerShape(20.dp))
                    ) {
                        AdsBannerMedium(modifier = Modifier.fillMaxWidth())
                    }
                } else {
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(Color.White, RoundedCornerShape(20.dp))
                    ) {
                        AdsWidget(
                            context = this@OverlayService,
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
        }
    }

    // Trick The ComposeView into thinking we are tracking lifecycle
    val viewModelStore = ViewModelStore()
    val viewModelStoreOwner = object : ViewModelStoreOwner {
        override val viewModelStore: ViewModelStore
            get() = viewModelStore
    }
    val lifecycleOwner = MyLifecycleOwner()
    lifecycleOwner.performRestore(null)
    lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
    lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
    composeView.setViewTreeLifecycleOwner(lifecycleOwner)
    composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
    composeView.setViewTreeViewModelStoreOwner(viewModelStoreOwner)

    windowManager.addView(composeView, params)
}

override fun onBind(intent: Intent): IBinder? {
    return null
}

}

I tried multiple solutions but nothing worked for me, the last thing I tried was setting a listener key on the view itself like this

composeView.requestFocus()
    composeView.setFocusableInTouchMode(true)
    composeView.setOnKeyListener(View.OnKeyListener { v, keyCode, event ->

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            stopSelf()
            return@OnKeyListener true
        }
        false
    })

But unfortunately, I didn't get notified at all and that didn't work as well. I looked into some questions here in the site like those:

Is possible remove an overlay with WindowManager when press back or home button in android? - stackoverflow

I tried the correct answer of that question but it always crashes asking for a token so I guess this answer would work only if the activity is not destroyed which is not my case because my compose is an overlay that works even if the app is closed

Service with Overlay - catch back button press - stackoverflow

I tried the solution of key listener which I previously mentioned that was in this question post and still didn't work for me and didn't get notified at all.


Solution

  • I found a solution for this problem that works for API's from 28 to 34.

    The solution was to remove "FLAG_NOT_FOCUSABLE" flag from here

     val layoutFlag: Int =
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
    
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.MATCH_PARENT,
            layoutFlag,
            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN ,
            PixelFormat.TRANSLUCENT
        )
    

    and then we setup a listener with our view like this:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            composeView.addOnUnhandledKeyEventListener{ view, event ->
    
                if ( event.action == KeyEvent.KEYCODE_BACK  || event.action == KeyEvent.KEYCODE_SOFT_LEFT || event.action == KeyEvent.KEYCODE_SOFT_RIGHT || event.action == KeyEvent.KEYCODE_HOME || event.action == KeyEvent.KEYCODE_MOVE_HOME) {
                    PreviousCallCard.finishedShowingCard(this)
                    windowManager.removeView(composeView)
                    [email protected]()
                    true
                } else {
                    false
                }
            }
        }
    

    Since Apis above 28 have the soft left or right as a way to get back instead of the back button we handle all those keyCodes to handle the back effect.

    I still don't know if that an accurate and well implemented or not so if anyone has any enhancement I would be happy to see your contribution.