WindowAreaControllerImpl.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.window.area

import android.app.Activity
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.window.core.BuildConfig
import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.VerificationMode
import androidx.window.extensions.area.WindowAreaComponent
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE
import androidx.window.extensions.core.util.function.Consumer
import java.util.concurrent.Executor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged

/**
 * Implementation of WindowAreaController for devices
 * that do implement the WindowAreaComponent on device.
 *
 * Requires [Build.VERSION_CODES.N] due to the use of [Consumer].
 * Will not be created though on API levels lower than
 * [Build.VERSION_CODES.S] as that's the min level of support for
 * this functionality.
 */
@ExperimentalWindowApi
@RequiresApi(Build.VERSION_CODES.N)
internal class WindowAreaControllerImpl(
    private val windowAreaComponent: WindowAreaComponent
) : WindowAreaController {

    private var currentStatus: WindowAreaStatus? = null

    override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
        return callbackFlow {
            val listener = Consumer<@WindowAreaComponent.WindowAreaStatus Int> { status ->
                currentStatus = WindowAreaAdapter.translate(status)
                channel.trySend(currentStatus ?: WindowAreaStatus.UNSUPPORTED)
            }
            windowAreaComponent.addRearDisplayStatusListener(listener)
            awaitClose {
                windowAreaComponent.removeRearDisplayStatusListener(listener)
            }
        }.distinctUntilChanged()
    }

    override fun rearDisplayMode(
        activity: Activity,
        executor: Executor,
        windowAreaSessionCallback: WindowAreaSessionCallback
    ) {
        // If we already have a status value that is not [WindowAreaStatus.AVAILABLE]
        // we should throw an exception quick to indicate they tried to enable
        // RearDisplay mode when it was not available.
        if (currentStatus != null && currentStatus != WindowAreaStatus.AVAILABLE) {
            throw UnsupportedOperationException("Rear Display mode cannot be enabled currently")
        }
        val rearDisplaySessionConsumer =
            RearDisplaySessionConsumer(executor, windowAreaSessionCallback, windowAreaComponent)
        windowAreaComponent.startRearDisplaySession(activity, rearDisplaySessionConsumer)
    }

    internal class RearDisplaySessionConsumer(
        private val executor: Executor,
        private val appCallback: WindowAreaSessionCallback,
        private val extensionsComponent: WindowAreaComponent
    ) : Consumer<@WindowAreaComponent.WindowAreaSessionState Int> {

        private var session: WindowAreaSession? = null

        override fun accept(t: @WindowAreaComponent.WindowAreaSessionState Int) {
            when (t) {
                SESSION_STATE_ACTIVE -> onSessionStarted()
                SESSION_STATE_INACTIVE -> onSessionFinished()
                else -> {
                    if (BuildConfig.verificationMode == VerificationMode.STRICT) {
                        Log.d(TAG, "Received an unknown session status value: $t")
                    }
                    onSessionFinished()
                }
            }
        }

        private fun onSessionStarted() {
            session = RearDisplaySessionImpl(extensionsComponent)
            session?.let { executor.execute { appCallback.onSessionStarted(it) } }
        }

        private fun onSessionFinished() {
            session = null
            executor.execute { appCallback.onSessionEnded() }
        }
    }

    internal companion object {
        private val TAG = WindowAreaControllerImpl::class.simpleName
    }
}