EmbeddingAdapter.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.
 */

package androidx.window.embedding

import android.util.Pair as AndroidPair
import androidx.window.extensions.embedding.ActivityRule as OEMActivityRule
import androidx.window.extensions.embedding.ActivityRule.Builder as ActivityRuleBuilder
import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
import androidx.window.extensions.embedding.SplitAttributes.SplitType as OEMSplitType
import androidx.window.extensions.embedding.SplitAttributesCalculatorParams as OEMSplitAttributesCalculatorParams
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
import androidx.window.extensions.embedding.SplitPairRule as OEMSplitPairRule
import androidx.window.extensions.embedding.SplitPairRule.Builder as SplitPairRuleBuilder
import androidx.window.extensions.embedding.SplitPlaceholderRule as OEMSplitPlaceholderRule
import androidx.window.extensions.embedding.SplitPlaceholderRule.Builder as SplitPlaceholderRuleBuilder
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.util.LayoutDirection
import android.view.WindowMetrics
import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
import androidx.window.embedding.SplitAttributes.SplitType
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
import androidx.window.embedding.SplitAttributes.SplitType.Companion.ratio
import androidx.window.extensions.WindowExtensions
import androidx.window.extensions.core.util.function.Function
import androidx.window.extensions.core.util.function.Predicate
import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType
import androidx.window.extensions.embedding.SplitPairRule.FINISH_ADJACENT
import androidx.window.extensions.embedding.SplitPairRule.FINISH_ALWAYS
import androidx.window.extensions.embedding.SplitPairRule.FINISH_NEVER
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
import kotlin.Pair

/**
 * Adapter class that translates data classes between Extension and Jetpack interfaces.
 */
internal class EmbeddingAdapter(
    private val predicateAdapter: PredicateAdapter
) {
    private val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
    private val api1Impl = VendorApiLevel1Impl(predicateAdapter)
    private val api2Impl = VendorApiLevel2Impl()

    fun translate(splitInfoList: List<OEMSplitInfo>): List<SplitInfo> {
        return splitInfoList.map(this::translate)
    }

    private fun translate(splitInfo: OEMSplitInfo): SplitInfo {
        return when (vendorApiLevel) {
            WindowExtensions.VENDOR_API_LEVEL_1 -> api1Impl.translateCompat(splitInfo)
            WindowExtensions.VENDOR_API_LEVEL_2 -> api2Impl.translateCompat(splitInfo)
            else -> {
                val primaryActivityStack = splitInfo.primaryActivityStack
                val secondaryActivityStack = splitInfo.secondaryActivityStack
                SplitInfo(
                    ActivityStack(
                        primaryActivityStack.activities,
                        primaryActivityStack.isEmpty
                    ),
                    ActivityStack(
                        secondaryActivityStack.activities,
                        secondaryActivityStack.isEmpty
                    ),
                    translate(splitInfo.splitAttributes)
                )
            }
        }
    }

    internal fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes =
        SplitAttributes.Builder()
            .setSplitType(
                when (val splitType = splitAttributes.splitType) {
                    is OEMSplitType.HingeSplitType -> SPLIT_TYPE_HINGE
                    is OEMSplitType.ExpandContainersSplitType -> SPLIT_TYPE_EXPAND
                    is RatioSplitType -> ratio(splitType.ratio)
                    else -> throw IllegalArgumentException("Unknown split type: $splitType")
                }
            ).setLayoutDirection(
                when (val layoutDirection = splitAttributes.layoutDirection) {
                    OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT -> LEFT_TO_RIGHT
                    OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT -> RIGHT_TO_LEFT
                    OEMSplitAttributes.LayoutDirection.LOCALE -> LOCALE
                    OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM -> TOP_TO_BOTTOM
                    OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP -> BOTTOM_TO_TOP
                    else -> throw IllegalArgumentException(
                        "Unknown layout direction: $layoutDirection"
                    )
                }
            )
            .build()

    @OptIn(ExperimentalWindowApi::class)
    fun translateSplitAttributesCalculator(
        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
    ): Function<OEMSplitAttributesCalculatorParams, OEMSplitAttributes> = Function { oemParams ->
            translateSplitAttributes(calculator.invoke(translate(oemParams)))
        }

    @OptIn(ExperimentalWindowApi::class)
    @SuppressLint("NewApi")
    fun translate(
        params: OEMSplitAttributesCalculatorParams
    ): SplitAttributesCalculatorParams = let {
        val taskWindowMetrics = params.parentWindowMetrics
        val taskConfiguration = params.parentConfiguration
        val windowLayoutInfo = params.parentWindowLayoutInfo
        val defaultSplitAttributes = params.defaultSplitAttributes
        val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
        val splitRuleTag = params.splitRuleTag
        val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics)

        SplitAttributesCalculatorParams(
            windowMetrics,
            taskConfiguration,
            ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
            translate(defaultSplitAttributes),
            areDefaultConstraintsSatisfied,
            splitRuleTag,
        )
    }

    private fun translateSplitPairRule(
        context: Context,
        rule: SplitPairRule,
        predicateClass: Class<*>
    ): OEMSplitPairRule {
        if (vendorApiLevel < WindowExtensions.VENDOR_API_LEVEL_2) {
            return api1Impl.translateSplitPairRuleCompat(context, rule, predicateClass)
        } else {
            val activitiesPairPredicate =
                Predicate<AndroidPair<Activity, Activity>> { activitiesPair ->
                    rule.filters.any { filter ->
                        filter.matchesActivityPair(activitiesPair.first, activitiesPair.second)
                    }
                }
            val activityIntentPredicate =
                Predicate<AndroidPair<Activity, Intent>> { activityIntentPair ->
                    rule.filters.any { filter ->
                        filter.matchesActivityIntentPair(
                            activityIntentPair.first,
                            activityIntentPair.second
                        )
                    }
                }
            val windowMetricsPredicate = Predicate<WindowMetrics> { windowMetrics ->
                rule.checkParentMetrics(context, windowMetrics)
            }
            val tag = rule.tag
            val builder = SplitPairRuleBuilder(
                activitiesPairPredicate,
                activityIntentPredicate,
                windowMetricsPredicate,
            )
                .setDefaultSplitAttributes(translateSplitAttributes(rule.defaultSplitAttributes))
                .setFinishPrimaryWithSecondary(
                    translateFinishBehavior(rule.finishPrimaryWithSecondary)
                ).setFinishSecondaryWithPrimary(
                    translateFinishBehavior(rule.finishSecondaryWithPrimary)
                ).setShouldClearTop(rule.clearTop)

            if (tag != null) {
                builder.setTag(tag)
            }
            return builder.build()
        }
    }

    fun translateSplitAttributes(splitAttributes: SplitAttributes): OEMSplitAttributes {
        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2)
        // To workaround the "unused" error in ktlint. It is necessary to translate SplitAttributes
        // from WM Jetpack version to WM extension version.
        return androidx.window.extensions.embedding.SplitAttributes.Builder()
            .setSplitType(translateSplitType(splitAttributes.splitType))
            .setLayoutDirection(
                when (splitAttributes.layoutDirection) {
                    LOCALE -> OEMSplitAttributes.LayoutDirection.LOCALE
                    LEFT_TO_RIGHT -> OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT
                    RIGHT_TO_LEFT -> OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT
                    TOP_TO_BOTTOM -> OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM
                    BOTTOM_TO_TOP -> OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP
                    else -> throw IllegalArgumentException("Unsupported layoutDirection:" +
                        "$splitAttributes.layoutDirection"
                    )
                }
            )
            .build()
    }

    private fun translateSplitType(splitType: SplitType): OEMSplitType {
        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2)
        return when (splitType) {
            SPLIT_TYPE_HINGE -> OEMSplitType.HingeSplitType(
                translateSplitType(SPLIT_TYPE_EQUAL)
            )
            SPLIT_TYPE_EXPAND -> OEMSplitType.ExpandContainersSplitType()
            else -> {
                val ratio = splitType.value
                if (ratio > 0.0 && ratio < 1.0) {
                    RatioSplitType(ratio)
                } else {
                    throw IllegalArgumentException("Unsupported SplitType: $splitType with value:" +
                        " ${splitType.value}")
                }
            }
        }
    }

    private fun translateSplitPlaceholderRule(
        context: Context,
        rule: SplitPlaceholderRule,
        predicateClass: Class<*>
    ): OEMSplitPlaceholderRule {
        if (vendorApiLevel < WindowExtensions.VENDOR_API_LEVEL_2) {
            return api1Impl.translateSplitPlaceholderRuleCompat(
                context,
                rule,
                predicateClass
            )
        } else {
            val activityPredicate = Predicate<Activity> { activity ->
                rule.filters.any { filter -> filter.matchesActivity(activity) }
            }
            val intentPredicate = Predicate<Intent> { intent ->
                rule.filters.any { filter -> filter.matchesIntent(intent) }
            }
            val windowMetricsPredicate = Predicate<WindowMetrics> { windowMetrics ->
                rule.checkParentMetrics(context, windowMetrics)
            }
            val tag = rule.tag
            val builder = SplitPlaceholderRuleBuilder(
                rule.placeholderIntent,
                activityPredicate,
                intentPredicate,
                windowMetricsPredicate
            )
                .setSticky(rule.isSticky)
                .setDefaultSplitAttributes(translateSplitAttributes(rule.defaultSplitAttributes))
                .setFinishPrimaryWithPlaceholder(
                    translateFinishBehavior(rule.finishPrimaryWithPlaceholder)
                )
            if (tag != null) {
                builder.setTag(tag)
            }
            return builder.build()
        }
    }

    fun translateFinishBehavior(behavior: SplitRule.FinishBehavior): Int =
        when (behavior) {
            SplitRule.FinishBehavior.NEVER -> FINISH_NEVER
            SplitRule.FinishBehavior.ALWAYS -> FINISH_ALWAYS
            SplitRule.FinishBehavior.ADJACENT -> FINISH_ADJACENT
            else -> throw IllegalArgumentException("Unknown finish behavior:$behavior")
        }

    private fun translateActivityRule(
        rule: ActivityRule,
        predicateClass: Class<*>
    ): OEMActivityRule {
        if (vendorApiLevel < WindowExtensions.VENDOR_API_LEVEL_2) {
            return api1Impl.translateActivityRuleCompat(rule, predicateClass)
        } else {
            val activityPredicate = Predicate<Activity> { activity ->
                rule.filters.any { filter -> filter.matchesActivity(activity) }
            }
            val intentPredicate = Predicate<Intent> { intent ->
                rule.filters.any { filter -> filter.matchesIntent(intent) }
            }
            val builder = ActivityRuleBuilder(activityPredicate, intentPredicate)
                .setShouldAlwaysExpand(rule.alwaysExpand)
            val tag = rule.tag
            if (tag != null) {
                builder.setTag(tag)
            }
            return builder.build()
        }
    }

    fun translate(context: Context, rules: Set<EmbeddingRule>): Set<OEMEmbeddingRule> {
        val predicateClass = predicateAdapter.predicateClassOrNull() ?: return emptySet()
        return rules.map { rule ->
            when (rule) {
                is SplitPairRule -> translateSplitPairRule(context, rule, predicateClass)
                is SplitPlaceholderRule ->
                    translateSplitPlaceholderRule(context, rule, predicateClass)
                is ActivityRule -> translateActivityRule(rule, predicateClass)
                else -> throw IllegalArgumentException("Unsupported rule type")
            }
        }.toSet()
    }

    private inner class VendorApiLevel2Impl {
        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo {
            val primaryActivityStack = splitInfo.primaryActivityStack
            val primaryFragment = ActivityStack(
                primaryActivityStack.activities,
                primaryActivityStack.isEmpty
            )

            val secondaryActivityStack = splitInfo.secondaryActivityStack
            val secondaryFragment = ActivityStack(
                secondaryActivityStack.activities,
                secondaryActivityStack.isEmpty
            )
            return SplitInfo(
                primaryFragment,
                secondaryFragment,
                translate(splitInfo.splitAttributes)
            )
        }
    }

    /**
     * Provides backward compatibility for Window extensions with
     * [WindowExtensions.VENDOR_API_LEVEL_1]
     * @see WindowExtensions.getVendorApiLevel
     */
    // Suppress deprecation because this object is to provide backward compatibility.
    @Suppress("DEPRECATION")
    private inner class VendorApiLevel1Impl(val predicateAdapter: PredicateAdapter) {
        /**
         * Obtains [SplitAttributes] from [OEMSplitInfo] with [WindowExtensions.VENDOR_API_LEVEL_1]
         */
        fun getSplitAttributesCompat(splitInfo: OEMSplitInfo): SplitAttributes =
            SplitAttributes.Builder()
                .setSplitType(SplitType.buildSplitTypeFromValue(splitInfo.splitRatio))
                .setLayoutDirection(LOCALE)
                .build()

        fun translateActivityRuleCompat(
            rule: ActivityRule,
            predicateClass: Class<*>
        ): OEMActivityRule = ActivityRuleBuilder::class.java.getConstructor(
                predicateClass,
                predicateClass
            ).newInstance(
                translateActivityPredicates(rule.filters),
                translateIntentPredicates(rule.filters)
            )
                .setShouldAlwaysExpand(rule.alwaysExpand)
                .build()

        fun translateSplitPlaceholderRuleCompat(
            context: Context,
            rule: SplitPlaceholderRule,
            predicateClass: Class<*>
        ): OEMSplitPlaceholderRule = SplitPlaceholderRuleBuilder::class.java.getConstructor(
                Intent::class.java,
                predicateClass,
                predicateClass,
                predicateClass
            ).newInstance(
                rule.placeholderIntent,
                translateActivityPredicates(rule.filters),
                translateIntentPredicates(rule.filters),
                translateParentMetricsPredicate(context, rule)
            )
                .setSticky(rule.isSticky)
                .setFinishPrimaryWithSecondary(
                    translateFinishBehavior(rule.finishPrimaryWithPlaceholder)
                ).setDefaultSplitAttributesCompat(rule.defaultSplitAttributes)
                .build()

        private fun SplitPlaceholderRuleBuilder.setDefaultSplitAttributesCompat(
            defaultAttrs: SplitAttributes,
        ): SplitPlaceholderRuleBuilder = apply {
            val (splitRatio, layoutDirection) = translateSplitAttributesCompatInternal(defaultAttrs)
            // #setDefaultAttributes or SplitAttributes ctr weren't supported.
            setSplitRatio(splitRatio)
            setLayoutDirection(layoutDirection)
        }

        fun translateSplitPairRuleCompat(
            context: Context,
            rule: SplitPairRule,
            predicateClass: Class<*>
        ): OEMSplitPairRule = SplitPairRuleBuilder::class.java.getConstructor(
                predicateClass,
                predicateClass,
                predicateClass,
            ).newInstance(
                translateActivityPairPredicates(rule.filters),
                translateActivityIntentPredicates(rule.filters),
                translateParentMetricsPredicate(context, rule)
            )
                .setDefaultSplitAttributesCompat(rule.defaultSplitAttributes)
                .setShouldClearTop(rule.clearTop)
                .setFinishPrimaryWithSecondary(
                    translateFinishBehavior(rule.finishPrimaryWithSecondary)
                ).setFinishSecondaryWithPrimary(
                    translateFinishBehavior(rule.finishSecondaryWithPrimary)
                ).build()

        @SuppressLint("ClassVerificationFailure", "NewApi")
        private fun translateActivityPairPredicates(splitPairFilters: Set<SplitPairFilter>): Any {
            return predicateAdapter.buildPairPredicate(
                Activity::class,
                Activity::class
            ) { first: Activity, second: Activity ->
                splitPairFilters.any { filter -> filter.matchesActivityPair(first, second) }
            }
        }

        @SuppressLint("ClassVerificationFailure", "NewApi")
        private fun translateActivityIntentPredicates(splitPairFilters: Set<SplitPairFilter>): Any {
            return predicateAdapter.buildPairPredicate(
                Activity::class,
                Intent::class
            ) { first, second ->
                splitPairFilters.any { filter -> filter.matchesActivityIntentPair(first, second) }
            }
        }

        private fun SplitPairRuleBuilder.setDefaultSplitAttributesCompat(
            defaultAttrs: SplitAttributes,
        ): SplitPairRuleBuilder = apply {
            val (splitRatio, layoutDirection) = translateSplitAttributesCompatInternal(defaultAttrs)
            setSplitRatio(splitRatio)
            setLayoutDirection(layoutDirection)
        }

        private fun translateSplitAttributesCompatInternal(
            attrs: SplitAttributes
        ): Pair<Float, Int> = // Use a (Float, Integer) pair since SplitAttributes weren't supported
            if (!isSplitAttributesSupported(attrs)) {
                // Fallback to expand the secondary container if the SplitAttributes are not
                // supported.
                Pair(0.0f, LayoutDirection.LOCALE)
            } else {
                Pair(
                    attrs.splitType.value,
                    when (attrs.layoutDirection) {
                        // Legacy LayoutDirection uses LayoutDirection constants in framework APIs.
                        LOCALE -> LayoutDirection.LOCALE
                        LEFT_TO_RIGHT -> LayoutDirection.LTR
                        RIGHT_TO_LEFT -> LayoutDirection.RTL
                        else -> throw IllegalStateException("Unsupported layout direction must be" +
                            " covered in @isSplitAttributesSupported!")
                    }
                )
            }

        /**
         * Returns `true` if `attrs` is compatible with [WindowExtensions.VENDOR_API_LEVEL_1] and
         * doesn't use the new features introduced in [WindowExtensions.VENDOR_API_LEVEL_2] or
         * higher.
         */
        private fun isSplitAttributesSupported(attrs: SplitAttributes) =
            attrs.splitType.value in 0.0..1.0 && attrs.splitType.value != 1.0f &&
                attrs.layoutDirection in arrayOf(LEFT_TO_RIGHT, RIGHT_TO_LEFT, LOCALE)

        @SuppressLint("ClassVerificationFailure", "NewApi")
        private fun translateActivityPredicates(activityFilters: Set<ActivityFilter>): Any {
            return predicateAdapter.buildPredicate(Activity::class) { activity ->
                activityFilters.any { filter -> filter.matchesActivity(activity) }
            }
        }

        @SuppressLint("ClassVerificationFailure", "NewApi")
        private fun translateIntentPredicates(activityFilters: Set<ActivityFilter>): Any {
            return predicateAdapter.buildPredicate(Intent::class) { intent ->
                activityFilters.any { filter -> filter.matchesIntent(intent) }
            }
        }

        @SuppressLint("ClassVerificationFailure", "NewApi")
        private fun translateParentMetricsPredicate(context: Context, splitRule: SplitRule): Any =
            predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
                splitRule.checkParentMetrics(context, windowMetrics)
            }

        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo = SplitInfo(
                ActivityStack(
                    splitInfo.primaryActivityStack.activities,
                    splitInfo.primaryActivityStack.isEmpty,
                ),
                ActivityStack(
                    splitInfo.secondaryActivityStack.activities,
                    splitInfo.secondaryActivityStack.isEmpty,
                ),
                getSplitAttributesCompat(splitInfo),
            )
    }
}