ExtensionEmbeddingBackend.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.app.Activity
import android.content.Context
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.collection.ArraySet
import androidx.core.util.Consumer
import androidx.window.core.ConsumerAdapter
import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

internal class ExtensionEmbeddingBackend @VisibleForTesting constructor(
    @field:VisibleForTesting @field:GuardedBy(
        "globalLock"
    ) var embeddingExtension: EmbeddingInterfaceCompat?
) : EmbeddingBackend {

    @VisibleForTesting
    val splitChangeCallbacks: CopyOnWriteArrayList<SplitListenerWrapper>
    private val splitInfoEmbeddingCallback = EmbeddingCallbackImpl()

    init {
        splitChangeCallbacks = CopyOnWriteArrayList<SplitListenerWrapper>()
        embeddingExtension?.setEmbeddingCallback(splitInfoEmbeddingCallback)
    }

    companion object {
        @Volatile
        private var globalInstance: ExtensionEmbeddingBackend? = null
        private val globalLock = ReentrantLock()
        private const val TAG = "EmbeddingBackend"

        fun getInstance(applicationContext: Context): ExtensionEmbeddingBackend {
            if (globalInstance == null) {
                globalLock.withLock {
                    if (globalInstance == null) {
                        val embeddingExtension = initAndVerifyEmbeddingExtension(applicationContext)
                        globalInstance = ExtensionEmbeddingBackend(embeddingExtension)
                    }
                }
            }
            return globalInstance!!
        }

        /**
         * Loads an instance of [androidx.window.extensions.embedding.ActivityEmbeddingComponent]
         * implemented by OEM if available on this device. This also verifies if the loaded
         * implementation conforms to the declared API version.
         */
        private fun initAndVerifyEmbeddingExtension(
            applicationContext: Context
        ): EmbeddingInterfaceCompat? {
            var impl: EmbeddingInterfaceCompat? = null
            try {
                if (isExtensionVersionSupported(ExtensionsUtil.safeVendorApiLevel) &&
                    EmbeddingCompat.isEmbeddingAvailable()
                ) {
                    impl = EmbeddingBackend::class.java.classLoader?.let { loader ->
                        EmbeddingCompat(
                            EmbeddingCompat.embeddingComponent(),
                            EmbeddingAdapter(PredicateAdapter(loader)),
                            ConsumerAdapter(loader),
                            applicationContext
                        )
                    }
                    // TODO(b/190433400): Check API conformance
                }
            } catch (t: Throwable) {
                if (EmbeddingCompat.DEBUG) {
                    Log.d(TAG, "Failed to load embedding extension: $t")
                }
                impl = null
            }
            if (impl == null) {
                if (EmbeddingCompat.DEBUG) {
                    Log.d(TAG, "No supported embedding extension found")
                }
            }
            return impl
        }

        /**
         * Checks if the Extension version provided on this device is supported by the current
         * version of the library.
         */
        @VisibleForTesting
        fun isExtensionVersionSupported(extensionVersion: Int?): Boolean {
            if (extensionVersion == null) {
                return false
            }

            return extensionVersion >= 1
        }
    }

    @GuardedBy("globalLock")
    private val ruleTracker = RuleTracker()

    @GuardedBy("globalLock")
    override fun getRules(): Set<EmbeddingRule> {
        globalLock.withLock { return ruleTracker.splitRules }
    }

    @GuardedBy("globalLock")
    override fun setRules(rules: Set<EmbeddingRule>) {
        globalLock.withLock {
            ruleTracker.setRules(rules)
            embeddingExtension?.setRules(getRules())
        }
    }

    @GuardedBy("globalLock")
    override fun addRule(rule: EmbeddingRule) {
        globalLock.withLock {
            if (rule !in ruleTracker) {
                ruleTracker.addOrUpdateRule(rule)
                embeddingExtension?.setRules(getRules())
            }
        }
    }

    @GuardedBy("globalLock")
    override fun removeRule(rule: EmbeddingRule) {
        globalLock.withLock {
            if (rule in ruleTracker) {
                ruleTracker.removeRule(rule)
                embeddingExtension?.setRules(getRules())
            }
        }
    }

    /**
     * A helper class to manage the registered [tags][EmbeddingRule.tag] and [rules][EmbeddingRule]
     * It supports:
     *   - Add a set of [rules][EmbeddingRule] and verify if there's duplicated [EmbeddingRule.tag]
     *     if needed.
     *   - Clears all registered [rules][EmbeddingRule]
     *   - Add a runtime [rule][EmbeddingRule] or update an existing [rule][EmbeddingRule] by
     *   [tag][EmbeddingRule.tag] if the tag has been registered.
     *   - Remove a runtime [rule][EmbeddingRule]
     */
    private class RuleTracker {
        val splitRules = ArraySet<EmbeddingRule>()
        private val tagRuleMap = HashMap<String, EmbeddingRule>()

        fun setRules(rules: Set<EmbeddingRule>) {
            clearRules()
            rules.forEach { rule -> addOrUpdateRule(rule, throwOnDuplicateTag = true) }
        }

        fun clearRules() {
            splitRules.clear()
            tagRuleMap.clear()
        }

        /**
         * Adds a rule to [RuleTracker] or update an existing rule if the [tag][EmbeddingRule.tag]
         * has been registered and `throwOnDuplicateTag` is `false`
         * @throws IllegalArgumentException if `throwOnDuplicateTag` is `true` and the
         * [tag][EmbeddingRule.tag] has been registered.
         */
        fun addOrUpdateRule(rule: EmbeddingRule, throwOnDuplicateTag: Boolean = false) {
            if (rule in splitRules) {
                return
            }
            val tag = rule.tag
            if (tag == null) {
                splitRules.add(rule)
            } else if (tagRuleMap.containsKey(tag)) {
                if (throwOnDuplicateTag) {
                    throw IllegalArgumentException(
                        "Duplicated tag: $tag. Tag must be unique " +
                            "among all registered rules"
                    )
                } else {
                    // Update the rule if throwOnDuplicateTag = false
                    val oldRule = tagRuleMap[tag]
                    splitRules.remove(oldRule)
                    tagRuleMap[tag] = rule
                    splitRules.add(rule)
                }
            } else {
                tagRuleMap[tag] = rule
                splitRules.add(rule)
            }
        }

        fun removeRule(rule: EmbeddingRule) {
            if (rule !in splitRules) {
                return
            }
            splitRules.remove(rule)
            val tag = rule.tag
            if (tag != null) {
                tagRuleMap.remove(rule.tag)
            }
        }

        operator fun contains(rule: EmbeddingRule): Boolean {
            return splitRules.contains(rule)
        }
    }

    /**
     * Wrapper around [Consumer<List<SplitInfo>>] that also includes the [Executor]
     * on which the callback should run and the [Activity].
     */
    internal class SplitListenerWrapper(
        private val activity: Activity,
        private val executor: Executor,
        val callback: Consumer<List<SplitInfo>>
    ) {
        private var lastValue: List<SplitInfo>? = null
        fun accept(splitInfoList: List<SplitInfo>) {
            val splitsWithActivity = splitInfoList.filter { splitState ->
                splitState.contains(activity)
            }
            if (splitsWithActivity == lastValue) {
                return
            }
            lastValue = splitsWithActivity
            executor.execute { callback.accept(splitsWithActivity) }
        }
    }

    override fun addSplitListenerForActivity(
        activity: Activity,
        executor: Executor,
        callback: Consumer<List<SplitInfo>>
    ) {
        globalLock.withLock {
            if (embeddingExtension == null) {
                if (EmbeddingCompat.DEBUG) {
                    Log.v(TAG, "Extension not loaded, skipping callback registration.")
                }
                callback.accept(emptyList())
                return
            }

            val callbackWrapper = SplitListenerWrapper(activity, executor, callback)
            splitChangeCallbacks.add(callbackWrapper)
            if (splitInfoEmbeddingCallback.lastInfo != null) {
                callbackWrapper.accept(splitInfoEmbeddingCallback.lastInfo!!)
            } else {
                callbackWrapper.accept(emptyList())
            }
        }
    }

    override fun removeSplitListenerForActivity(
        consumer: Consumer<List<SplitInfo>>
    ) {
        globalLock.withLock {
            for (callbackWrapper in splitChangeCallbacks) {
                if (callbackWrapper.callback == consumer) {
                    splitChangeCallbacks.remove(callbackWrapper)
                    break
                }
            }
        }
    }

    /**
     * Extension callback implementation of the split information. Keeps track of last reported
     * values.
     */
    internal inner class EmbeddingCallbackImpl : EmbeddingCallbackInterface {
        var lastInfo: List<SplitInfo>? = null
        override fun onSplitInfoChanged(splitInfo: List<SplitInfo>) {
            lastInfo = splitInfo
            for (callbackWrapper in splitChangeCallbacks) {
                callbackWrapper.accept(splitInfo)
            }
        }
    }

    override fun isSplitSupported(): Boolean {
        return embeddingExtension != null
    }

    override fun isActivityEmbedded(activity: Activity): Boolean {
        return embeddingExtension?.isActivityEmbedded(activity) ?: false
    }

    override fun setSplitAttributesCalculator(
        calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
    ) {
        globalLock.withLock {
            embeddingExtension?.setSplitAttributesCalculator(calculator)
        }
    }

    override fun clearSplitAttributesCalculator() {
        globalLock.withLock {
            embeddingExtension?.clearSplitAttributesCalculator()
        }
    }

    override fun isSplitAttributesCalculatorSupported(): Boolean =
        embeddingExtension?.isSplitAttributesCalculatorSupported() ?: false
}