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.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.annotation.DoNotInline
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.collection.ArraySet
import androidx.core.util.Consumer
import androidx.window.WindowProperties
import androidx.window.core.BuildConfig
import androidx.window.core.ConsumerAdapter
import androidx.window.core.ExperimentalWindowApi
import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
import androidx.window.core.VerificationMode
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
import androidx.window.embedding.ExtensionEmbeddingBackend.Api31Impl.isSplitPropertyEnabled
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(
    private val applicationContext: Context,
    @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(context: Context): EmbeddingBackend {
            if (globalInstance == null) {
                globalLock.withLock {
                    if (globalInstance == null) {
                        val applicationContext = context.applicationContext
                        val embeddingExtension = initAndVerifyEmbeddingExtension(applicationContext)
                        globalInstance = ExtensionEmbeddingBackend(
                            applicationContext,
                            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)
            }
        }
    }

    private fun areExtensionsAvailable(): Boolean {
        return embeddingExtension != null
    }

    override val splitSupportStatus: SplitController.SplitSupportStatus by lazy {
        when {
            !areExtensionsAvailable() -> {
                SplitController.SplitSupportStatus.SPLIT_UNAVAILABLE
            }
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
                isSplitPropertyEnabled(applicationContext)
            }
            else -> {
                // The PackageManager#getProperty API is not supported before S, assuming
                // the property is enabled to keep the same behavior on earlier platforms.
                SplitController.SplitSupportStatus.SPLIT_AVAILABLE
            }
        }
    }

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

    @ExperimentalWindowApi
    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

    @RequiresApi(31)
    private object Api31Impl {
        @DoNotInline
        fun isSplitPropertyEnabled(context: Context): SplitController.SplitSupportStatus {
            val property = try {
                context.packageManager.getProperty(
                    WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED,
                    context.packageName
                )
            } catch (e: PackageManager.NameNotFoundException) {
                if (BuildConfig.verificationMode == VerificationMode.LOG) {
                    Log.w(TAG, WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED +
                            " must be set and enabled in AndroidManifest.xml to use splits APIs."
                    )
                }
                return SplitController.SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED
            } catch (e: Exception) {
                if (BuildConfig.verificationMode == VerificationMode.LOG) {
                    // This can happen when it is a test environment that doesn't support
                    // getProperty.
                    Log.e(TAG, "PackageManager.getProperty is not supported", e)
                }
                return SplitController.SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED
            }
            if (!property.isBoolean) {
                if (BuildConfig.verificationMode == VerificationMode.LOG) {
                    Log.w(TAG, WindowProperties.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED +
                            " must have a boolean value"
                    )
                }
                return SplitController.SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED
            }
            return if (property.boolean) {
                SplitController.SplitSupportStatus.SPLIT_AVAILABLE
            } else {
                SplitController.SplitSupportStatus.SPLIT_UNAVAILABLE
            }
        }
    }
}