/*
* Copyright 2021 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.Binder
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNKNOWN
import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
import androidx.window.area.utils.DeviceUtils
import androidx.window.core.BuildConfig
import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.VerificationMode
import androidx.window.extensions.area.ExtensionWindowAreaStatus
import androidx.window.extensions.area.WindowAreaComponent
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_ACTIVE
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE
import androidx.window.extensions.area.WindowAreaComponent.SESSION_STATE_INACTIVE
import androidx.window.extensions.area.WindowAreaComponent.WindowAreaSessionState
import androidx.window.extensions.core.util.function.Consumer
import androidx.window.layout.WindowMetrics
import androidx.window.layout.WindowMetricsCalculator
import java.util.concurrent.Executor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/**
* 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.Q)
internal class WindowAreaControllerImpl(
private val windowAreaComponent: WindowAreaComponent,
private val vendorApiLevel: Int
) : WindowAreaController {
private lateinit var rearDisplaySessionConsumer: Consumer<Int>
private var currentRearDisplayModeStatus: WindowAreaCapability.Status =
WINDOW_AREA_STATUS_UNKNOWN
private var currentRearDisplayPresentationStatus: WindowAreaCapability.Status =
WINDOW_AREA_STATUS_UNKNOWN
private val currentWindowAreaInfoMap = HashMap<String, WindowAreaInfo>()
override val windowAreaInfos: Flow<List<WindowAreaInfo>>
get() {
return callbackFlow {
val rearDisplayListener = Consumer<Int> { status ->
updateRearDisplayAvailability(status)
channel.trySend(currentWindowAreaInfoMap.values.toList())
}
val rearDisplayPresentationListener =
Consumer<ExtensionWindowAreaStatus> { extensionWindowAreaStatus ->
updateRearDisplayPresentationAvailability(extensionWindowAreaStatus)
channel.trySend(currentWindowAreaInfoMap.values.toList())
}
windowAreaComponent.addRearDisplayStatusListener(rearDisplayListener)
if (vendorApiLevel > 2) {
windowAreaComponent.addRearDisplayPresentationStatusListener(
rearDisplayPresentationListener
)
}
awaitClose {
windowAreaComponent.removeRearDisplayStatusListener(rearDisplayListener)
if (vendorApiLevel > 2) {
windowAreaComponent.removeRearDisplayPresentationStatusListener(
rearDisplayPresentationListener
)
}
}
}
}
private fun updateRearDisplayAvailability(
status: @WindowAreaComponent.WindowAreaStatus Int
) {
val windowMetrics = if (vendorApiLevel >= 3) {
WindowMetricsCalculator.fromDisplayMetrics(
displayMetrics = windowAreaComponent.rearDisplayMetrics
)
} else {
val displayMetrics = DeviceUtils.getRearDisplayMetrics(Build.MANUFACTURER, Build.MODEL)
if (displayMetrics != null) {
WindowMetricsCalculator.fromDisplayMetrics(
displayMetrics = displayMetrics
)
} else {
throw IllegalArgumentException(
"DeviceUtils rear display metrics entry should not be null"
)
}
}
currentRearDisplayModeStatus = WindowAreaAdapter.translate(status)
updateRearDisplayWindowArea(
WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA,
currentRearDisplayModeStatus,
windowMetrics
)
}
private fun updateRearDisplayPresentationAvailability(
extensionWindowAreaStatus: ExtensionWindowAreaStatus
) {
currentRearDisplayPresentationStatus =
WindowAreaAdapter.translate(extensionWindowAreaStatus.windowAreaStatus)
val windowMetrics = WindowMetricsCalculator.fromDisplayMetrics(
displayMetrics = extensionWindowAreaStatus.windowAreaDisplayMetrics
)
updateRearDisplayWindowArea(
WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA,
currentRearDisplayPresentationStatus,
windowMetrics,
)
}
/**
* Updates the [WindowAreaInfo] object with the [REAR_DISPLAY_BINDER_DESCRIPTOR] binder token
* with the updated [status] corresponding to the [operation] and with the updated [metrics]
* received from the device for this window area.
*
* @param operation Operation that we are updating the status of.
* @param status New status for the operation provided on this window area.
* @param metrics Updated [WindowMetrics] for this window area.
*/
private fun updateRearDisplayWindowArea(
operation: WindowAreaCapability.Operation,
status: WindowAreaCapability.Status,
metrics: WindowMetrics,
) {
var rearDisplayAreaInfo: WindowAreaInfo? =
currentWindowAreaInfoMap[REAR_DISPLAY_BINDER_DESCRIPTOR]
if (status == WINDOW_AREA_STATUS_UNSUPPORTED) {
rearDisplayAreaInfo?.let { info ->
if (shouldRemoveWindowAreaInfo(info)) {
currentWindowAreaInfoMap.remove(REAR_DISPLAY_BINDER_DESCRIPTOR)
} else {
val capability = WindowAreaCapability(operation, status)
info.capabilityMap[operation] = capability
}
}
} else {
if (rearDisplayAreaInfo == null) {
rearDisplayAreaInfo = WindowAreaInfo(
metrics = metrics,
type = WindowAreaInfo.Type.TYPE_REAR_FACING,
// TODO(b/273807238): Update extensions to send the binder token and type
token = Binder(REAR_DISPLAY_BINDER_DESCRIPTOR),
windowAreaComponent = windowAreaComponent
)
}
val capability = WindowAreaCapability(operation, status)
rearDisplayAreaInfo.capabilityMap[operation] = capability
rearDisplayAreaInfo.metrics = metrics
currentWindowAreaInfoMap[REAR_DISPLAY_BINDER_DESCRIPTOR] = rearDisplayAreaInfo
}
}
/**
* Determines if a [WindowAreaInfo] should be removed from [windowAreaInfos] if all
* [WindowAreaCapability] are currently [WINDOW_AREA_STATUS_UNSUPPORTED]
*/
private fun shouldRemoveWindowAreaInfo(windowAreaInfo: WindowAreaInfo): Boolean {
for (capability: WindowAreaCapability in windowAreaInfo.capabilityMap.values) {
if (capability.status != WINDOW_AREA_STATUS_UNSUPPORTED) {
return false
}
}
return true
}
override fun transferActivityToWindowArea(
token: Binder,
activity: Activity,
executor: Executor,
windowAreaSessionCallback: WindowAreaSessionCallback
) {
if (token.interfaceDescriptor != REAR_DISPLAY_BINDER_DESCRIPTOR) {
executor.execute {
windowAreaSessionCallback.onSessionEnded(
IllegalArgumentException("Invalid WindowAreaInfo token"))
}
return
}
if (currentRearDisplayModeStatus == WINDOW_AREA_STATUS_UNKNOWN) {
Log.d(TAG, "Force updating currentRearDisplayModeStatus")
// currentRearDisplayModeStatus may be null if the client has not queried
// WindowAreaController.windowAreaInfos using this instance. In this case, we query
// it for a single value to force update currentRearDisplayModeStatus.
CoroutineScope(executor.asCoroutineDispatcher()).launch {
windowAreaInfos.first()
startRearDisplayMode(activity, executor, windowAreaSessionCallback)
}
} else {
startRearDisplayMode(activity, executor, windowAreaSessionCallback)
}
}
override fun presentContentOnWindowArea(
token: Binder,
activity: Activity,
executor: Executor,
windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
) {
if (token.interfaceDescriptor != REAR_DISPLAY_BINDER_DESCRIPTOR) {
executor.execute {
windowAreaPresentationSessionCallback.onSessionEnded(
IllegalArgumentException("Invalid WindowAreaInfo token"))
}
return
}
if (currentRearDisplayPresentationStatus == WINDOW_AREA_STATUS_UNKNOWN) {
Log.d(TAG, "Force updating currentRearDisplayPresentationStatus")
// currentRearDisplayModeStatus may be null if the client has not queried
// WindowAreaController.windowAreaInfos using this instance. In this case, we query
// it for a single value to force update currentRearDisplayPresentationStatus.
CoroutineScope(executor.asCoroutineDispatcher()).launch {
windowAreaInfos.first()
startRearDisplayPresentationMode(
activity,
executor,
windowAreaPresentationSessionCallback
)
}
} else {
startRearDisplayPresentationMode(
activity,
executor,
windowAreaPresentationSessionCallback
)
}
}
private fun startRearDisplayMode(
activity: Activity,
executor: Executor,
windowAreaSessionCallback: WindowAreaSessionCallback
) {
// If the capability is currently active, provide an error pointing the developer on how to
// get access to the current session
if (currentRearDisplayModeStatus == WINDOW_AREA_STATUS_ACTIVE) {
windowAreaSessionCallback.onSessionEnded(
IllegalStateException(
"The WindowArea feature is currently active, WindowAreaInfo#getActiveSession" +
"can be used to get an instance of the current active session"
)
)
return
}
// If we already have an availability value that is not
// [Availability.WINDOW_AREA_CAPABILITY_AVAILABLE] we should end the session and pass an
// exception to indicate they tried to enable rear display mode when it was not available.
if (currentRearDisplayModeStatus != WINDOW_AREA_STATUS_AVAILABLE) {
windowAreaSessionCallback.onSessionEnded(
IllegalStateException(
"The WindowArea feature is currently not available to be entered"
)
)
return
}
rearDisplaySessionConsumer =
RearDisplaySessionConsumer(executor, windowAreaSessionCallback, windowAreaComponent)
windowAreaComponent.startRearDisplaySession(activity, rearDisplaySessionConsumer)
}
private fun startRearDisplayPresentationMode(
activity: Activity,
executor: Executor,
windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback
) {
if (currentRearDisplayPresentationStatus != WINDOW_AREA_STATUS_AVAILABLE) {
windowAreaPresentationSessionCallback.onSessionEnded(
IllegalStateException(
"The WindowArea feature is currently not available to be entered"
)
)
return
}
windowAreaComponent.startRearDisplayPresentationSession(
activity,
RearDisplayPresentationSessionConsumer(
executor,
windowAreaPresentationSessionCallback,
windowAreaComponent
)
)
}
internal class RearDisplaySessionConsumer(
private val executor: Executor,
private val appCallback: WindowAreaSessionCallback,
private val extensionsComponent: WindowAreaComponent
) : Consumer<Int> {
private var session: WindowAreaSession? = null
override fun accept(t: 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(null) }
}
}
internal class RearDisplayPresentationSessionConsumer(
private val executor: Executor,
private val windowAreaPresentationSessionCallback: WindowAreaPresentationSessionCallback,
private val windowAreaComponent: WindowAreaComponent
) : Consumer<@WindowAreaSessionState Int> {
private var lastReportedSessionStatus: @WindowAreaSessionState Int = SESSION_STATE_INACTIVE
override fun accept(t: @WindowAreaSessionState Int) {
val previousStatus: @WindowAreaSessionState Int = lastReportedSessionStatus
lastReportedSessionStatus = t
executor.execute {
when (t) {
SESSION_STATE_ACTIVE -> {
// If the last status was visible, then ACTIVE infers the content is no
// longer visible.
if (previousStatus == SESSION_STATE_CONTENT_VISIBLE) {
windowAreaPresentationSessionCallback.onContainerVisibilityChanged(
false /* isVisible */
)
} else {
// Presentation should never be null if the session is active
windowAreaPresentationSessionCallback.onSessionStarted(
RearDisplayPresentationSessionPresenterImpl(
windowAreaComponent,
windowAreaComponent.rearDisplayPresentation!!
)
)
}
}
SESSION_STATE_CONTENT_VISIBLE ->
windowAreaPresentationSessionCallback.onContainerVisibilityChanged(true)
SESSION_STATE_INACTIVE ->
windowAreaPresentationSessionCallback.onSessionEnded(null)
else -> {
Log.e(TAG, "Invalid session state value received: $t")
}
}
}
}
}
internal companion object {
private val TAG = WindowAreaControllerImpl::class.simpleName
private const val REAR_DISPLAY_BINDER_DESCRIPTOR = "WINDOW_AREA_REAR_DISPLAY"
}
}