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