IdleEventBroadcastReceiver.kt

/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.glance.session

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.PowerManager
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

internal class IdleEventBroadcastReceiver(val onIdle: () -> Unit) : BroadcastReceiver() {
    companion object {
        val events = listOf(
            PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED,
            PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED,
            PowerManager.ACTION_LOW_POWER_STANDBY_ENABLED_CHANGED
        )
        val filter = IntentFilter().apply {
            events.forEach { addAction(it) }
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action in events)
            checkIdleStatus(context)
    }

    internal fun checkIdleStatus(context: Context) {
        // Idle status is not available before Android M
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return

        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
        var isIdle = Api23Impl.isIdle(pm)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            isIdle = isIdle || Api33Impl.isLightIdleOrLowPowerStandby(pm)
        }
        if (isIdle) onIdle()
    }
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private object Api33Impl {
    @DoNotInline
    fun isLightIdleOrLowPowerStandby(pm: PowerManager): Boolean {
        return pm.isLowPowerStandbyEnabled || pm.isDeviceLightIdleMode
    }
}

@RequiresApi(Build.VERSION_CODES.M)
private object Api23Impl {
    @DoNotInline
    fun isIdle(pm: PowerManager): Boolean {
        return pm.isDeviceIdleMode
    }
}

/**
 * Observe idle events while running [block]. If the device enters idle mode, run [onIdle].
 */
internal suspend fun <T> observeIdleEvents(
    context: Context,
    onIdle: suspend () -> Unit,
    block: suspend () -> T,
): T = coroutineScope {
    val idleReceiver = IdleEventBroadcastReceiver { launch { onIdle() } }
    context.registerReceiver(idleReceiver, IdleEventBroadcastReceiver.filter)
    try {
        idleReceiver.checkIdleStatus(context)
        return@coroutineScope block()
    } finally {
        context.unregisterReceiver(idleReceiver)
    }
}