https://kotlinlang.org logo
#compose
Title
# compose
s

Stylianos Gakis

03/11/2022, 11:32 AM
I had been interested in making a part of my composable App keep the screen awake. Reading though these docs it shows that all the approaches include either an Activity, or services etc. has anyone had to do this before? Any ideas on how to approach it?
I was thinking of doing something like
Copy code
@Composable
fun AwakeScreen(content: @Composable () -> Unit) {
  val window: Window? = null// can I get access to the current window here? Or some way to get the hosting activity which holds the window?
  DisposableEffect(Unit) {
    window!!.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
    onDispose { 
      window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
    }
  }
  content()
}
But I am not sure we have access to this inside our composables. Would I have to somehow hoist this all the way up to the hosting activity? Anything smarter I should be able to do perhaps?
Hmm this works actually
Copy code
@Composable
fun AwakeScreen(content: @Composable () -> Unit) {
    val context = LocalContext.current
    DisposableEffect(Unit) {
        val powerManager = context.getSystemService(PowerManager::class.java)
        val wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "myapp:wakelock")
        wakeLock.acquire(10.minutes.inWholeMilliseconds)
        onDispose {
            wakeLock.release()
        }
    }
    content()
}
However
PowerManager.SCREEN_DIM_WAKE_LOCK
is deprecated suggesting to use the
FLAG_KEEP_SCREEN_ON
on the window as I said before. Can’t find a way to do that so will use the deprecated option for now 😅
a

Albert Chang

03/11/2022, 12:16 PM
You are abusing wake lock. Here’s how you can access the window.
s

Stylianos Gakis

03/11/2022, 12:22 PM
This seems to work as well, that’s great!
Copy code
@Composable
fun AwakeScreen(content: @Composable () -> Unit) {
  val context = LocalContext.current
  DisposableEffect(Unit) {
    val window = context.findWindow()
    window!!.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
    onDispose {
      window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
    }
  }
  content()
}

private fun Context.findWindow(): Window? {
  var context = this
  while (context is ContextWrapper) {
    if (context is Activity) return context.window
    context = context.baseContext
  }
  return null
}
Now I gotta do a
!!
or at least a
?
on the window though, which either way doesn’t make me feel that great. I guess accessing it like this from a composable has no way of failing since it will always find the activity though right? I’d like to at least document that in that function if so, so my colleagues don’t need to worry about this not working.
Also
LocalContext.current.findWindow()
and
LocalView.current.context.findWindow()
should probably be equivalent since this is how they’re provided so that’s not important I assume
a

Albert Chang

03/11/2022, 12:44 PM
Using
LocalView
(in a library) is safer as some people are overriding the context with their own ones (e.g. to use a locale different from the default one).
s

Stylianos Gakis

03/11/2022, 12:46 PM
Right, that’s smart! Thanks so much for saving me twice from doing something wrong 😂🤗
a

Adam Powell

03/11/2022, 3:41 PM
you don't need the window for this since View has a keepScreenOn property of its own
the view hierarchy automatically aggregates this within the window and handles it for you
if you want to do this robustly from compose you'll want to refcount it so that multiple instances of something turning it on and off don't step on one another though
something like this would do it: (untested code)
Copy code
private val View.keepScreenOnState: KeepScreenOnState
    get() = getTag(R.id.keep_screen_on_state) as? KeepScreenOnState
        ?: KeepScreenOnState(this).also { setTag(R.id.keep_screen_on_state, it) }

private class KeepScreenOnState(private val view: View) {
    private var refCount = 0
        set(value) {
            val newValue = value.coerceAtLeast(0)
            field = newValue
            view.keepScreenOn = newValue > 0
        }
    
    fun request() {
        refCount++
    }
    
    fun release() {
        refCount--
    }
}

@Composable
fun KeepScreenOnRequest() {
    val view = LocalView.current
    DisposableEffect(view) {
        val ksoState = view.keepScreenOnState
        ksoState.request()
        onDispose {
            ksoState.release()
        }
    }
}
and then declare the resource id
keep_screen_on_state
in an
ids.xml
resource file so that the
R
constant used above is there
this approach maps to the
android:keepScreenOn="true"
approach noted in the docs you linked above; that's the view property mentioned
s

Stylianos Gakis

03/11/2022, 4:44 PM
Oh alright, didn’t know keepScreenOn existed on the View itself as well. Considering these two would be functionally equivalent, this feels like a much more involved process and I’d need to add this to my codebase probably with a link to this chat because otherwise whoever sees this would wonder why I didn’t just do the “normal” approach. If this is the way to do this in compose, maybe it’s worth a spot in the documentation?
a

Adam Powell

03/11/2022, 4:46 PM
probably worth adding the above to one of the official libraries, really. But there's nothing about it that isn't already documented in the link you posted
👍 2
s

Stylianos Gakis

03/11/2022, 4:53 PM
This would be perfect to exist in one of the official libraries, indeed! Thank you for your help!
👍 1
d

Dejan Predovic

03/11/2022, 6:42 PM
Copy code
@Composable
fun KeepScreenOn() {
  val activity = LocalContext.current as Activity
  DisposableEffect(Unit) {
    activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
    onDispose { activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) }
  }
}
Works perfectly for me
a

Adam Powell

03/12/2022, 3:17 AM
it will work in isolation only so long as there are either 0 or 1 of that composable present at a time. Once there's more than one at a time for any reason (and that can include things like animated transitions between screens) then you can get into trouble if you're not scoping and reference counting it
d

Dejan Predovic

03/12/2022, 1:16 PM
Hmmm.. You are right, something like this should do the trick, right?
Copy code
val keepScreenOnCounter = AtomicInteger(0)

@Composable
fun KeepScreenOn() {
  val activity = LocalContext.current as Activity
  DisposableEffect(Unit) {
    val counter = keepScreenOnCounter.incrementAndGet()
    if (counter == 1) activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
    onDispose {
      val counter = keepScreenOnCounter.decrementAndGet()
      if (counter == 0) activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
    }
  }
}
a

Adam Powell

03/12/2022, 2:16 PM
Nope, because the activity window flags are per-activity window and what you have there is global. Plus if you have any other UI in the window it may also fight over the flag. I'm curious, why not use what I pasted above? That snippet will scope the changes to the ComposeView you're working with.
☝️ 1
☝🏽 1
s

Stylianos Gakis

03/14/2022, 9:42 AM
Could this have been a Modifier btw? I find it a bit awkward to have a random composable which doesn’t emit any UI, nor does it return a value, so it’s basically a side effect. Maybe it’s just because I am not used to doing something like this but anyway. I’ve looked into the Modifier documentation and I can’t find too much about how to make our own one. Not quite sure even if this use case fits a Modifier, or how one would write one like that. It’s just that on the call-site if I slapped a
.keepScreenOn
in the modifier chain of a composable I think it’d feel more natural.
Aha just this seems to work actually
Copy code
fun Modifier.keepScreenOn() = composed {
  KeepScreenOnRequest()
  this
}
Can anyone confirm that this is/isn’t fine?Not sure if I am breaking some Modifier rule though 😅 Can we just have whatever composables as part of a
composed
modifier? Even ones that emit UI? Is there some part of the documentation that talks more about all this? This and this both don’t mention this, making me think this isn’t what I want to do for some reason?
a

Albert Chang

03/14/2022, 11:47 AM
If it's a modifier, what does it modify?
s

Stylianos Gakis

03/14/2022, 12:04 PM
It modifies its behavior on the screen, and the flag of the current View it is inside, isn’t that a modification? There are definitely modifiers that don’t modify the composable size/behavior itself, like for example
onGloballyPositioned
or
onKeyEvent
which alters the keyboard behavior.
a

Albert Chang

03/14/2022, 12:06 PM
A modifier modifies the layout it's applied to. It doesn't modify itself.
1
s

Stylianos Gakis

03/14/2022, 12:13 PM
Hmm, yes that’s true. It just feels quite natural to be able to write
SomeComposable(modifier = Modifier.keepScreenOn())
as I feel like this is modifying my
SomeComposable
by making it keep the screen on, just like I’d expect all other modifiers to work. Especially compared to having to put a
KeepScreenOnRequest()
at the top of a composable as a side effect at the spot I’d also put a
SideEffect
or smth like that. Maybe if I found a better name for it it’d feel less weird.
Maybe the best solution is for it to act like a wrapper (like MaterialTheme does for example) with a signature like:
Copy code
@Composable
fun ScreenOn(content: @Composable () -> Unit) {
  ... same code as Adam posted
  content()
}
where I can add a `ScreenOn() { myComposables() } and just take the extra indentation
a

Albert Chang

03/14/2022, 12:19 PM
It's basically the same as
BackHandler()
. I think you just have to get used to it.
s

Stylianos Gakis

03/14/2022, 12:22 PM
TIL
BackHandler
exists. And you’re right, it’s the exact same approach. Should probably just keep it like that then! Thanks for helping me out on this one everyone!
👍 1
7 Views