1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.ui.text.platform 18 19 import androidx.annotation.VisibleForTesting 20 import androidx.compose.runtime.State 21 import androidx.compose.runtime.mutableStateOf 22 import androidx.emoji2.text.EmojiCompat 23 24 /** 25 * Tests may provide alternative global implementations for [EmojiCompatStatus] using this delegate. 26 */ 27 internal interface EmojiCompatStatusDelegate { 28 val fontLoaded: State<Boolean> 29 } 30 31 /** Used for observing emojicompat font loading status from compose. */ 32 internal object EmojiCompatStatus : EmojiCompatStatusDelegate { 33 private var delegate: EmojiCompatStatusDelegate = DefaultImpl() 34 35 /** 36 * True if the emoji2 font is currently loaded and processing will be successful 37 * 38 * False when emoji2 may complete loading in the future. 39 */ 40 override val fontLoaded: State<Boolean> 41 get() = delegate.fontLoaded 42 43 /** 44 * Do not call. 45 * 46 * This is for tests that want to control EmojiCompatStatus behavior. 47 */ 48 @VisibleForTesting setDelegateForTestingnull49 internal fun setDelegateForTesting(newDelegate: EmojiCompatStatusDelegate?) { 50 delegate = newDelegate ?: DefaultImpl() 51 } 52 } 53 54 /** is-a state, but doesn't cause an observation when read */ 55 private class ImmutableBool(override val value: Boolean) : State<Boolean> 56 57 private val Falsey = ImmutableBool(false) 58 59 private class DefaultImpl : EmojiCompatStatusDelegate { 60 61 private var loadState: State<Boolean>? 62 63 init { 64 loadState = 65 if (EmojiCompat.isConfigured()) { 66 getFontLoadState() 67 } else { 68 // EC isn't configured yet, will check again in getter 69 null 70 } 71 } 72 73 override val fontLoaded: State<Boolean> 74 get() = 75 if (loadState != null) { 76 loadState!! 77 } else { 78 // EC wasn't configured last time, check again and update loadState if it's ready 79 if (EmojiCompat.isConfigured()) { 80 loadState = getFontLoadState() 81 loadState!! 82 } else { 83 // ec disabled path 84 // no observations allowed, this is pre init 85 Falsey 86 } 87 } 88 getFontLoadStatenull89 private fun getFontLoadState(): State<Boolean> { 90 val ec = EmojiCompat.get() 91 return if (ec.loadState == EmojiCompat.LOAD_STATE_SUCCEEDED) { 92 ImmutableBool(true) 93 } else { 94 val mutableLoaded = mutableStateOf(false) 95 val initCallback = 96 object : EmojiCompat.InitCallback() { 97 override fun onInitialized() { 98 mutableLoaded.value = true // update previous observers 99 loadState = ImmutableBool(true) // never observe again 100 } 101 102 override fun onFailed(throwable: Throwable?) { 103 loadState = Falsey // never observe again 104 } 105 } 106 ec.registerInitCallback(initCallback) 107 mutableLoaded 108 } 109 } 110 } 111