SidecarAdapter.kt
/*
* 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.
*/
@file:Suppress("DEPRECATION")
package androidx.window.layout
import android.annotation.SuppressLint
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.window.core.Bounds
import androidx.window.layout.ExtensionWindowBackend.Companion.DEBUG
import androidx.window.sidecar.SidecarDeviceState
import androidx.window.sidecar.SidecarDisplayFeature
import androidx.window.sidecar.SidecarWindowLayoutInfo
import java.lang.reflect.InvocationTargetException
/**
* A class for translating Sidecar data classes.
*/
internal class SidecarAdapter {
fun translate(
sidecarDisplayFeatures: List<SidecarDisplayFeature>,
deviceState: SidecarDeviceState
): List<DisplayFeature> {
return sidecarDisplayFeatures.mapNotNull { sidecarFeature ->
translate(sidecarFeature, deviceState)
}
}
fun translate(
extensionInfo: SidecarWindowLayoutInfo?,
state: SidecarDeviceState
): WindowLayoutInfo {
if (extensionInfo == null) {
return WindowLayoutInfo(emptyList())
}
val deviceState = SidecarDeviceState()
val devicePosture = getSidecarDevicePosture(state)
setSidecarDevicePosture(deviceState, devicePosture)
val sidecarDisplayFeatures = getSidecarDisplayFeatures(extensionInfo)
val displayFeatures = translate(sidecarDisplayFeatures, deviceState)
return WindowLayoutInfo(displayFeatures)
}
fun isEqualSidecarDeviceState(
first: SidecarDeviceState?,
second: SidecarDeviceState?
): Boolean {
if (first == second) {
return true
}
if (first == null) {
return false
}
if (second == null) {
return false
}
val firstPosture = getSidecarDevicePosture(first)
val secondPosture = getSidecarDevicePosture(second)
return firstPosture == secondPosture
}
fun isEqualSidecarWindowLayoutInfo(
first: SidecarWindowLayoutInfo?,
second: SidecarWindowLayoutInfo?
): Boolean {
if (first == second) {
return true
}
if (first == null) {
return false
}
if (second == null) {
return false
}
val firstDisplayFeatures = getSidecarDisplayFeatures(first)
val secondDisplayFeatures = getSidecarDisplayFeatures(second)
return isEqualSidecarDisplayFeatures(firstDisplayFeatures, secondDisplayFeatures)
}
private fun isEqualSidecarDisplayFeatures(
first: List<SidecarDisplayFeature>?,
second: List<SidecarDisplayFeature>?
): Boolean {
if (first === second) {
return true
}
if (first == null) {
return false
}
if (second == null) {
return false
}
if (first.size != second.size) {
return false
}
for (i in first.indices) {
val firstFeature = first[i]
val secondFeature = second[i]
if (!isEqualSidecarDisplayFeature(firstFeature, secondFeature)) {
return false
}
}
return true
}
private fun isEqualSidecarDisplayFeature(
first: SidecarDisplayFeature?,
second: SidecarDisplayFeature?
): Boolean {
if (first == second) {
return true
}
if (first == null) {
return false
}
if (second == null) {
return false
}
if (first.type != second.type) {
return false
}
return if (first.rect != second.rect) {
false
} else {
true
}
}
companion object {
private val TAG = SidecarAdapter::class.java.simpleName
// TODO(b/172620880): Workaround for Sidecar API implementation issue.
@SuppressLint("BanUncheckedReflection")
@VisibleForTesting
fun getSidecarDisplayFeatures(info: SidecarWindowLayoutInfo): List<SidecarDisplayFeature> {
try {
return info.displayFeatures ?: emptyList()
} catch (error: NoSuchFieldError) {
try {
val methodGetFeatures = SidecarWindowLayoutInfo::class.java.getMethod(
"getDisplayFeatures"
)
@Suppress("UNCHECKED_CAST")
return methodGetFeatures.invoke(info) as List<SidecarDisplayFeature>
} catch (e: NoSuchMethodException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: IllegalAccessException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: InvocationTargetException) {
if (DEBUG) {
e.printStackTrace()
}
}
}
return emptyList()
}
// TODO(b/172620880): Workaround for Sidecar API implementation issue.
@SuppressLint("BanUncheckedReflection")
@VisibleForTesting
fun setSidecarDisplayFeatures(
info: SidecarWindowLayoutInfo,
displayFeatures: List<SidecarDisplayFeature?>
) {
try {
info.displayFeatures = displayFeatures
} catch (error: NoSuchFieldError) {
try {
val methodSetFeatures = SidecarWindowLayoutInfo::class.java.getMethod(
"setDisplayFeatures", MutableList::class.java
)
methodSetFeatures.invoke(info, displayFeatures)
} catch (e: NoSuchMethodException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: IllegalAccessException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: InvocationTargetException) {
if (DEBUG) {
e.printStackTrace()
}
}
}
}
internal fun getSidecarDevicePosture(sidecarDeviceState: SidecarDeviceState): Int {
val rawPosture = getRawSidecarDevicePosture(sidecarDeviceState)
return if (rawPosture < SidecarDeviceState.POSTURE_UNKNOWN ||
rawPosture > SidecarDeviceState.POSTURE_FLIPPED
) {
SidecarDeviceState.POSTURE_UNKNOWN
} else {
rawPosture
}
}
// TODO(b/172620880): Workaround for Sidecar API implementation issue.
@SuppressLint("BanUncheckedReflection")
@VisibleForTesting
fun getRawSidecarDevicePosture(sidecarDeviceState: SidecarDeviceState): Int {
try {
return sidecarDeviceState.posture
} catch (error: NoSuchFieldError) {
try {
val methodGetPosture = SidecarDeviceState::class.java.getMethod("getPosture")
return methodGetPosture.invoke(sidecarDeviceState) as Int
} catch (e: NoSuchMethodException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: IllegalAccessException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: InvocationTargetException) {
if (DEBUG) {
e.printStackTrace()
}
}
}
return SidecarDeviceState.POSTURE_UNKNOWN
}
// TODO(b/172620880): Workaround for Sidecar API implementation issue.
@SuppressLint("BanUncheckedReflection")
@VisibleForTesting
fun setSidecarDevicePosture(sidecarDeviceState: SidecarDeviceState, posture: Int) {
try {
sidecarDeviceState.posture = posture
} catch (error: NoSuchFieldError) {
try {
val methodSetPosture = SidecarDeviceState::class.java.getMethod(
"setPosture",
Int::class.javaPrimitiveType
)
methodSetPosture.invoke(sidecarDeviceState, posture)
} catch (e: NoSuchMethodException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: IllegalAccessException) {
if (DEBUG) {
e.printStackTrace()
}
} catch (e: InvocationTargetException) {
if (DEBUG) {
e.printStackTrace()
}
}
}
}
/**
* Converts the display feature from extension. Can return `null` if there is an issue
* with the value passed from extension.
*/
internal fun translate(
feature: SidecarDisplayFeature,
deviceState: SidecarDeviceState
): DisplayFeature? {
val bounds = feature.rect
if (bounds.width() == 0 && bounds.height() == 0) {
if (DEBUG) {
Log.d(TAG, "Passed a display feature with empty rect, skipping: $feature")
}
return null
}
if (feature.type == SidecarDisplayFeature.TYPE_FOLD) {
if (bounds.width() != 0 && bounds.height() != 0) {
// Bounds for fold types are expected to be zero-wide or zero-high.
// See DisplayFeature#getBounds().
if (DEBUG) {
Log.d(
TAG,
"Passed a non-zero area display feature expected to be zero-area, " +
"skipping: $feature"
)
}
return null
}
}
if (feature.type == SidecarDisplayFeature.TYPE_HINGE ||
feature.type == SidecarDisplayFeature.TYPE_FOLD
) {
// TODO(b/175507310): Reinstate after fix on the OEM side.
if (
!(
bounds.left == 0 /* && bounds.right == windowBounds.width()*/ ||
bounds.top == 0 /* && bounds.bottom == windowBounds.height()*/
)
) {
// Bounds for fold and hinge types are expected to span the entire window space.
// See DisplayFeature#getBounds().
if (DEBUG) {
Log.d(
TAG,
"Passed a display feature expected to span the entire window but " +
"does not, skipping: $feature"
)
}
return null
}
}
val type = when (feature.type) {
SidecarDisplayFeature.TYPE_FOLD -> HardwareFoldingFeature.Type.FOLD
SidecarDisplayFeature.TYPE_HINGE -> HardwareFoldingFeature.Type.HINGE
else -> {
if (DEBUG) {
Log.d(TAG, "Unknown feature type: ${feature.type}, skipping feature.")
}
return null
}
}
val state = when (getSidecarDevicePosture(deviceState)) {
SidecarDeviceState.POSTURE_CLOSED,
SidecarDeviceState.POSTURE_UNKNOWN,
SidecarDeviceState.POSTURE_FLIPPED -> return null
SidecarDeviceState.POSTURE_HALF_OPENED -> FoldingFeature.State.HALF_OPENED
SidecarDeviceState.POSTURE_OPENED -> FoldingFeature.State.FLAT
else -> FoldingFeature.State.FLAT
}
return HardwareFoldingFeature(Bounds(feature.rect), type, state)
}
}
}