EmojiCompatStatus.kt

/*
 * Copyright 2022 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.compose.ui.text.platform

import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.emoji2.text.EmojiCompat

/**
 * Tests may provide alternative global implementations for [EmojiCompatStatus] using this delegate.
 */
internal interface EmojiCompatStatusDelegate {
    val fontLoaded: State<Boolean>
}

/**
 * Used for observing emojicompat font loading status from compose.
 */
internal object EmojiCompatStatus : EmojiCompatStatusDelegate {
    private var delegate: EmojiCompatStatusDelegate = DefaultImpl()

    /**
     * True if the emoji2 font is currently loaded and processing will be successful
     *
     * False when emoji2 may complete loading in the future.
     */
    override val fontLoaded: State<Boolean>
        get() = delegate.fontLoaded

    /**
     * Do not call.
     *
     * This is for tests that want to control EmojiCompatStatus behavior.
     */
    @VisibleForTesting
    internal fun setDelegateForTesting(newDelegate: EmojiCompatStatusDelegate?) {
        delegate = newDelegate ?: DefaultImpl()
    }
}

/**
 * is-a state, but doesn't cause an observation when read
 */
private class ImmutableBool(override val value: Boolean) : State<Boolean>
private val Falsey = ImmutableBool(false)

private class DefaultImpl : EmojiCompatStatusDelegate {

    private var loadState: State<Boolean>?

    init {
        loadState = if (EmojiCompat.isConfigured()) {
            getFontLoadState()
        } else {
            // EC isn't configured yet, will check again in getter
            null
        }
    }

    override val fontLoaded: State<Boolean>
        get() = if (loadState != null) {
            loadState!!
        } else {
            // EC wasn't configured last time, check again and update loadState if it's ready
            if (EmojiCompat.isConfigured()) {
                loadState = getFontLoadState()
                loadState!!
            } else {
                // ec disabled path
                // no observations allowed, this is pre init
                Falsey
            }
        }

    private fun getFontLoadState(): State<Boolean> {
        val ec = EmojiCompat.get()
        return if (ec.loadState == EmojiCompat.LOAD_STATE_SUCCEEDED) {
            ImmutableBool(true)
        } else {
            val mutableLoaded = mutableStateOf(false)
            val initCallback = object : EmojiCompat.InitCallback() {
                override fun onInitialized() {
                    mutableLoaded.value = true // update previous observers
                    loadState = ImmutableBool(true) // never observe again
                }

                override fun onFailed(throwable: Throwable?) {
                    loadState = Falsey // never observe again
                }
            }
            ec.registerInitCallback(initCallback)
            mutableLoaded
        }
    }
}