WindowAreaControllerJavaAdapter.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.java.area

import android.app.Activity
import androidx.core.util.Consumer
import androidx.window.area.WindowAreaSessionCallback
import androidx.window.area.WindowAreaStatus
import androidx.window.area.WindowAreaController
import androidx.window.core.ExperimentalWindowApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

/**
 * An adapted interface for [WindowAreaController] that provides the information and
 * functionality around RearDisplay Mode via a callback shaped API.
 *
 * @hide
 *
 */
@ExperimentalWindowApi
class WindowAreaControllerJavaAdapter(
    private val controller: WindowAreaController
) : WindowAreaController by controller {

    /**
     * A [ReentrantLock] to protect against concurrent access to [consumerToJobMap].
     */
    private val lock = ReentrantLock()
    private val consumerToJobMap = mutableMapOf<Consumer<*>, Job>()

    /**
     * Registers a listener to consume [WindowAreaStatus] values defined as
     * [WindowAreaStatus.UNSUPPORTED], [WindowAreaStatus.UNAVAILABLE], and
     * [WindowAreaStatus.AVAILABLE]. The values provided through this listener should be used
     * to determine if you are able to enable rear display Mode at that time. You can use these
     * values to modify your UI to show/hide controls and determine when to enable features
     * that use rear display Mode. You should only try and enter rear display mode when your
     * [consumer] is provided a value of [WindowAreaStatus.AVAILABLE].
     *
     * The [consumer] will be provided an initial value on registration, as well as any change
     * to the status as they occur. This could happen due to hardware device state changes, or if
     * another process has enabled RearDisplay Mode.
     *
     * @see WindowAreaController.rearDisplayStatus
     */
    fun addRearDisplayStatusListener(
        executor: Executor,
        consumer: Consumer<WindowAreaStatus>
    ) {
        val statusFlow = controller.rearDisplayStatus()
        lock.withLock {
            if (consumerToJobMap[consumer] == null) {
                val scope = CoroutineScope(executor.asCoroutineDispatcher())
                consumerToJobMap[consumer] = scope.launch {
                    statusFlow.collect { consumer.accept(it) }
                }
            }
        }
    }

    /**
     * Removes a listener of [WindowAreaStatus] values
     * @see WindowAreaController.rearDisplayStatus
     */
    fun removeRearDisplayStatusListener(consumer: Consumer<WindowAreaStatus>) {
        lock.withLock {
            consumerToJobMap[consumer]?.cancel()
            consumerToJobMap.remove(consumer)
        }
    }

    /**
     * Starts a RearDisplay Mode session and provides updates through the
     * [WindowAreaSessionCallback] provided. Due to the nature of moving your Activity to a
     * different display, your Activity will likely go through a configuration change. Because of
     * this, if your Activity does not override configuration changes, this method should be called
     * from a component that outlives the Activity lifecycle such as a
     * [androidx.lifecycle.ViewModel]. If your Activity does override
     * configuration changes, it is safe to call this method inside your Activity.
     *
     * This method should only be called if you have received a [WindowAreaStatus.AVAILABLE]
     * value from the listener provided through the [addRearDisplayStatusListener] method. If
     * you try and enable RearDisplay mode without it being available, you will receive an
     * [UnsupportedOperationException].
     *
     * The [windowAreaSessionCallback] provided will receive a call to
     * [WindowAreaSessionCallback.onSessionStarted] after your Activity has been moved to the
     * display corresponding to this mode. RearDisplay mode will stay active until the session
     * provided through [WindowAreaSessionCallback.onSessionStarted] is closed, or there is a device
     * state change that makes RearDisplay mode incompatible such as if the device is closed so the
     * outer-display is no longer in line with the rear camera. When this occurs,
     * [WindowAreaSessionCallback.onSessionEnded] is called to notify you the session has been
     * ended.
     *
     * @see addRearDisplayStatusListener
     * @throws UnsupportedOperationException if you try and start a RearDisplay session when
     * your [WindowAreaController.rearDisplayStatus] does not return a value of
     * [WindowAreaStatus.AVAILABLE]
     */
    fun startRearDisplayModeSession(
        activity: Activity,
        executor: Executor,
        windowAreaSessionCallback: WindowAreaSessionCallback
    ) {
        controller.rearDisplayMode(activity, executor, windowAreaSessionCallback)
    }
}