SafeActivityEmbeddingComponentProvider.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.embedding
import android.app.Activity
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.window.SafeWindowExtensionsProvider
import androidx.window.core.ConsumerAdapter
import androidx.window.core.ExtensionsUtil
import androidx.window.extensions.WindowExtensions
import androidx.window.extensions.core.util.function.Consumer
import androidx.window.extensions.core.util.function.Function
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
import androidx.window.extensions.embedding.ActivityRule
import androidx.window.extensions.embedding.ActivityStack
import androidx.window.extensions.embedding.SplitAttributes
import androidx.window.extensions.embedding.SplitAttributes.SplitType
import androidx.window.extensions.embedding.SplitInfo
import androidx.window.extensions.embedding.SplitPairRule
import androidx.window.extensions.embedding.SplitPlaceholderRule
import androidx.window.reflection.ReflectionUtils.doesReturn
import androidx.window.reflection.ReflectionUtils.isPublic
import androidx.window.reflection.ReflectionUtils.validateReflection
import androidx.window.reflection.WindowExtensionsConstants.ACTIVITY_EMBEDDING_COMPONENT_CLASS
/**
* Reflection Guard for [ActivityEmbeddingComponent].
* This will go through the [ActivityEmbeddingComponent]'s method by reflection and
* check each method's name and signature to see if the interface is what we required.
*/
internal class SafeActivityEmbeddingComponentProvider(
private val loader: ClassLoader,
private val consumerAdapter: ConsumerAdapter,
private val windowExtensions: WindowExtensions
) {
private val safeWindowExtensionsProvider = SafeWindowExtensionsProvider(loader)
val activityEmbeddingComponent: ActivityEmbeddingComponent?
get() {
return if (canUseActivityEmbeddingComponent()) {
try {
windowExtensions.activityEmbeddingComponent
} catch (e: UnsupportedOperationException) {
null
}
} else {
null
}
}
private fun canUseActivityEmbeddingComponent(): Boolean {
if (!isActivityEmbeddingComponentAccessible()) {
return false
}
// TODO(b/267573854) : update logic to fallback to lower version
// if higher version is not matched
return when (ExtensionsUtil.safeVendorApiLevel) {
1 -> hasValidVendorApiLevel1()
in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()
// TODO(b/267956499) : add hasValidVendorApiLevel3
else -> false
}
}
@VisibleForTesting
internal fun isActivityEmbeddingComponentAccessible(): Boolean =
safeWindowExtensionsProvider.isWindowExtensionsValid() &&
isActivityEmbeddingComponentValid()
/**
* [WindowExtensions.VENDOR_API_LEVEL_1] includes the following methods:
* - [ActivityEmbeddingComponent.setEmbeddingRules]
* - [ActivityEmbeddingComponent.isActivityEmbedded]
* - [ActivityEmbeddingComponent.setSplitInfoCallback] with [java.util.function.Consumer]
* and following classes:
* - [ActivityRule]
* - [SplitInfo]
* - [SplitPairRule]
* - [SplitPlaceholderRule]
*/
@VisibleForTesting
internal fun hasValidVendorApiLevel1(): Boolean {
return isMethodSetEmbeddingRulesValid() &&
isMethodIsActivityEmbeddedValid() &&
isMethodSetSplitInfoCallbackJavaConsumerValid() &&
isClassActivityRuleValid() &&
isClassSplitInfoValid() &&
isClassSplitPairRuleValid() &&
isClassSplitPlaceholderRuleValid()
}
/**
* Vendor API level 2 includes the following methods:
* - [ActivityEmbeddingComponent.setSplitInfoCallback] with [Consumer]
* - [ActivityEmbeddingComponent.clearSplitInfoCallback]
* - [ActivityEmbeddingComponent.setSplitAttributesCalculator]
* - [ActivityEmbeddingComponent.clearSplitAttributesCalculator]
* - [SplitInfo.getSplitAttributes]
* and following classes:
* - [SplitAttributes]
* - [SplitAttributes.SplitType]
*/
@VisibleForTesting
internal fun hasValidVendorApiLevel2(): Boolean {
return hasValidVendorApiLevel1() &&
isMethodSetSplitInfoCallbackWindowConsumerValid() &&
isMethodClearSplitInfoCallbackValid() &&
isMethodSplitAttributesCalculatorValid() &&
isMethodGetSplitAttributesValid() &&
isClassSplitAttributesValid() &&
isClassSplitTypeValid()
}
private fun isMethodSetEmbeddingRulesValid(): Boolean {
return validateReflection("ActivityEmbeddingComponent#setEmbeddingRules is not valid") {
val setEmbeddingRulesMethod = activityEmbeddingComponentClass.getMethod(
"setEmbeddingRules",
Set::class.java
)
setEmbeddingRulesMethod.isPublic
}
}
private fun isMethodIsActivityEmbeddedValid(): Boolean {
return validateReflection("ActivityEmbeddingComponent#isActivityEmbedded is not valid") {
val isActivityEmbeddedMethod = activityEmbeddingComponentClass.getMethod(
"isActivityEmbedded",
Activity::class.java
)
isActivityEmbeddedMethod.isPublic &&
isActivityEmbeddedMethod.doesReturn(Boolean::class.java)
}
}
private fun isMethodClearSplitInfoCallbackValid(): Boolean {
return validateReflection(
"ActivityEmbeddingComponent#clearSplitInfoCallback is not valid"
) {
val clearSplitInfoCallbackMethod =
activityEmbeddingComponentClass.getMethod("clearSplitInfoCallback")
clearSplitInfoCallbackMethod.isPublic
}
}
private fun isMethodSplitAttributesCalculatorValid(): Boolean {
return validateReflection(
"ActivityEmbeddingComponent#setSplitAttributesCalculator is not valid"
) {
val setSplitAttributesCalculatorMethod = activityEmbeddingComponentClass.getMethod(
"setSplitAttributesCalculator",
Function::class.java
)
val clearSplitAttributesCalculatorMethod =
activityEmbeddingComponentClass.getMethod("clearSplitAttributesCalculator")
setSplitAttributesCalculatorMethod.isPublic &&
clearSplitAttributesCalculatorMethod.isPublic
}
}
private fun isMethodGetSplitAttributesValid(): Boolean =
validateReflection("SplitInfo#getSplitAttributes is not valid") {
val splitInfoClass = SplitInfo::class.java
val getSplitAttributesMethod = splitInfoClass.getMethod("getSplitAttributes")
getSplitAttributesMethod.isPublic &&
getSplitAttributesMethod.doesReturn(SplitAttributes::class.java)
}
private fun isClassSplitAttributesValid(): Boolean =
validateReflection("Class SplitAttributes is not valid") {
val splitAttributesClass = SplitAttributes::class.java
val getLayoutDirectionMethod =
splitAttributesClass.getMethod("getLayoutDirection")
val getSplitTypeMethod = splitAttributesClass.getMethod("getSplitType")
val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
val setSplitTypeMethod = splitAttributesBuilderClass.getMethod(
"setSplitType",
SplitType::class.java
)
val setLayoutDirectionMethod = splitAttributesBuilderClass.getMethod(
"setLayoutDirection",
Int::class.java
)
getLayoutDirectionMethod.isPublic &&
getLayoutDirectionMethod.doesReturn(Int::class.java) &&
getSplitTypeMethod.isPublic &&
getSplitTypeMethod.doesReturn(SplitType::class.java) &&
setSplitTypeMethod.isPublic && setLayoutDirectionMethod.isPublic
}
private fun isClassSplitTypeValid(): Boolean =
validateReflection("Class SplitAttributes.SplitType is not valid") {
val ratioSplitTypeClass = SplitType.RatioSplitType::class.java
val ratioSplitTypeConstructor =
ratioSplitTypeClass.getDeclaredConstructor(Float::class.java)
val getRatioMethod = ratioSplitTypeClass.getMethod("getRatio")
val splitEquallyMethod = ratioSplitTypeClass.getMethod("splitEqually")
val hingeSplitTypeClass = SplitType.HingeSplitType::class.java
val hingeSplitTypeConstructor =
hingeSplitTypeClass.getDeclaredConstructor(SplitType::class.java)
val getFallbackSplitTypeMethod =
hingeSplitTypeClass.getMethod("getFallbackSplitType")
val expandContainersSplitTypeClass = SplitType.ExpandContainersSplitType::class.java
val expandContainersSplitTypeConstructor =
expandContainersSplitTypeClass.getDeclaredConstructor()
ratioSplitTypeConstructor.isPublic &&
getRatioMethod.isPublic &&
getRatioMethod.doesReturn(Float::class.java) &&
hingeSplitTypeConstructor.isPublic &&
splitEquallyMethod.isPublic &&
splitEquallyMethod.doesReturn(SplitType.RatioSplitType::class.java) &&
getFallbackSplitTypeMethod.isPublic &&
getFallbackSplitTypeMethod.doesReturn(SplitType::class.java) &&
expandContainersSplitTypeConstructor.isPublic
}
private fun isMethodSetSplitInfoCallbackJavaConsumerValid(): Boolean {
return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
val consumerClass =
consumerAdapter.consumerClassOrNull() ?: return@validateReflection false
val setSplitInfoCallbackMethod =
activityEmbeddingComponentClass.getMethod("setSplitInfoCallback", consumerClass)
setSplitInfoCallbackMethod.isPublic
}
}
private fun isClassActivityRuleValid(): Boolean =
validateReflection("Class ActivityRule is not valid") {
val activityRuleClass = ActivityRule::class.java
val shouldAlwaysExpandMethod = activityRuleClass.getMethod("shouldAlwaysExpand")
val activityRuleBuilderClass = ActivityRule.Builder::class.java
val setShouldAlwaysExpandMethod = activityRuleBuilderClass.getMethod(
"setShouldAlwaysExpand",
Boolean::class.java
)
shouldAlwaysExpandMethod.isPublic &&
shouldAlwaysExpandMethod.doesReturn(Boolean::class.java) &&
setShouldAlwaysExpandMethod.isPublic
}
private fun isClassSplitInfoValid(): Boolean =
validateReflection("Class SplitInfo is not valid") {
val splitInfoClass = SplitInfo::class.java
val getPrimaryActivityStackMethod =
splitInfoClass.getMethod("getPrimaryActivityStack")
val getSecondaryActivityStackMethod =
splitInfoClass.getMethod("getSecondaryActivityStack")
val getSplitRatioMethod = splitInfoClass.getMethod("getSplitRatio")
getPrimaryActivityStackMethod.isPublic &&
getPrimaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
getSecondaryActivityStackMethod.isPublic &&
getSecondaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
getSplitRatioMethod.isPublic &&
getSplitRatioMethod.doesReturn(Float::class.java)
}
private fun isClassSplitPairRuleValid(): Boolean =
validateReflection("Class SplitPairRule is not valid") {
val splitPairRuleClass = SplitPairRule::class.java
val getFinishPrimaryWithSecondaryMethod =
splitPairRuleClass.getMethod("getFinishPrimaryWithSecondary")
val getFinishSecondaryWithPrimaryMethod =
splitPairRuleClass.getMethod("getFinishSecondaryWithPrimary")
val shouldClearTopMethod = splitPairRuleClass.getMethod("shouldClearTop")
getFinishPrimaryWithSecondaryMethod.isPublic &&
getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java) &&
getFinishSecondaryWithPrimaryMethod.isPublic &&
getFinishSecondaryWithPrimaryMethod.doesReturn(Int::class.java) &&
shouldClearTopMethod.isPublic &&
shouldClearTopMethod.doesReturn(Boolean::class.java)
}
private fun isClassSplitPlaceholderRuleValid(): Boolean =
validateReflection("Class SplitPlaceholderRule is not valid") {
val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
val getPlaceholderIntentMethod =
splitPlaceholderRuleClass.getMethod("getPlaceholderIntent")
val isStickyMethod = splitPlaceholderRuleClass.getMethod("isSticky")
val getFinishPrimaryWithSecondaryMethod =
splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithSecondary")
getPlaceholderIntentMethod.isPublic &&
getPlaceholderIntentMethod.doesReturn(Intent::class.java) &&
isStickyMethod.isPublic &&
isStickyMethod.doesReturn(Boolean::class.java)
getFinishPrimaryWithSecondaryMethod.isPublic &&
getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java)
}
private fun isMethodSetSplitInfoCallbackWindowConsumerValid(): Boolean {
return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
val setSplitInfoCallbackMethod = activityEmbeddingComponentClass.getMethod(
"setSplitInfoCallback",
Consumer::class.java
)
setSplitInfoCallbackMethod.isPublic
}
}
private fun isActivityEmbeddingComponentValid(): Boolean {
return validateReflection("WindowExtensions#getActivityEmbeddingComponent is not valid") {
val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
val getActivityEmbeddingComponentMethod =
extensionsClass.getMethod("getActivityEmbeddingComponent")
val activityEmbeddingComponentClass = activityEmbeddingComponentClass
getActivityEmbeddingComponentMethod.isPublic &&
getActivityEmbeddingComponentMethod.doesReturn(activityEmbeddingComponentClass)
}
}
private val activityEmbeddingComponentClass: Class<*>
get() {
return loader.loadClass(ACTIVITY_EMBEDDING_COMPONENT_CLASS)
}
}