1 /* <lambda>null2 * Copyright 2021 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.font 18 19 import androidx.collection.LruCache 20 import androidx.compose.runtime.State 21 import androidx.compose.ui.text.platform.makeSynchronizedObject 22 import androidx.compose.ui.text.platform.synchronized 23 import androidx.compose.ui.util.fastMap 24 25 internal class FontFamilyResolverImpl( 26 internal val platformFontLoader: PlatformFontLoader /* exposed for desktop ParagraphBuilder */, 27 private val platformResolveInterceptor: PlatformResolveInterceptor = 28 PlatformResolveInterceptor.Default, 29 private val typefaceRequestCache: TypefaceRequestCache = GlobalTypefaceRequestCache, 30 private val fontListFontFamilyTypefaceAdapter: FontListFontFamilyTypefaceAdapter = 31 FontListFontFamilyTypefaceAdapter(GlobalAsyncTypefaceCache), 32 private val platformFamilyTypefaceAdapter: PlatformFontFamilyTypefaceAdapter = 33 PlatformFontFamilyTypefaceAdapter() 34 ) : FontFamily.Resolver { 35 private val createDefaultTypeface: (TypefaceRequest) -> Any = { 36 resolve(it.copy(fontFamily = null)).value 37 } 38 39 override suspend fun preload(fontFamily: FontFamily) { 40 // all other types of FontFamily are already preloaded. 41 if (fontFamily !is FontListFontFamily) return 42 43 fontListFontFamilyTypefaceAdapter.preload(fontFamily, platformFontLoader) 44 45 val typeRequests = 46 fontFamily.fonts.fastMap { 47 TypefaceRequest( 48 platformResolveInterceptor.interceptFontFamily(fontFamily), 49 platformResolveInterceptor.interceptFontWeight(it.weight), 50 platformResolveInterceptor.interceptFontStyle(it.style), 51 FontSynthesis.All, 52 platformFontLoader.cacheKey 53 ) 54 } 55 56 typefaceRequestCache.preWarmCache(typeRequests) { typeRequest -> 57 @Suppress("MoveLambdaOutsideParentheses") 58 fontListFontFamilyTypefaceAdapter.resolve( 59 typefaceRequest = typeRequest, 60 platformFontLoader = platformFontLoader, 61 onAsyncCompletion = { /* nothing */ }, 62 createDefaultTypeface = createDefaultTypeface 63 ) 64 ?: platformFamilyTypefaceAdapter.resolve( 65 typefaceRequest = typeRequest, 66 platformFontLoader = platformFontLoader, 67 onAsyncCompletion = { /* nothing */ }, 68 createDefaultTypeface = createDefaultTypeface 69 ) 70 ?: throw IllegalStateException("Could not load font") 71 } 72 } 73 74 override fun resolve( 75 fontFamily: FontFamily?, 76 fontWeight: FontWeight, 77 fontStyle: FontStyle, 78 fontSynthesis: FontSynthesis, 79 ): State<Any> { 80 return resolve( 81 TypefaceRequest( 82 platformResolveInterceptor.interceptFontFamily(fontFamily), 83 platformResolveInterceptor.interceptFontWeight(fontWeight), 84 platformResolveInterceptor.interceptFontStyle(fontStyle), 85 platformResolveInterceptor.interceptFontSynthesis(fontSynthesis), 86 platformFontLoader.cacheKey 87 ) 88 ) 89 } 90 91 /** Resolves the final [typefaceRequest] without interceptors. */ 92 private fun resolve(typefaceRequest: TypefaceRequest): State<Any> { 93 val result = 94 typefaceRequestCache.runCached(typefaceRequest) { onAsyncCompletion -> 95 fontListFontFamilyTypefaceAdapter.resolve( 96 typefaceRequest, 97 platformFontLoader, 98 onAsyncCompletion, 99 createDefaultTypeface 100 ) 101 ?: platformFamilyTypefaceAdapter.resolve( 102 typefaceRequest, 103 platformFontLoader, 104 onAsyncCompletion, 105 createDefaultTypeface 106 ) 107 ?: throw IllegalStateException("Could not load font") 108 } 109 return result 110 } 111 } 112 113 /** 114 * Platform level [FontFamily.Resolver] argument interceptor. This interface is intended to bridge 115 * accessibility constraints on any platform with Compose through the use of 116 * [FontFamilyResolverImpl.resolve]. 117 */ 118 internal interface PlatformResolveInterceptor { 119 interceptFontFamilynull120 fun interceptFontFamily(fontFamily: FontFamily?): FontFamily? = fontFamily 121 122 fun interceptFontWeight(fontWeight: FontWeight): FontWeight = fontWeight 123 124 fun interceptFontStyle(fontStyle: FontStyle): FontStyle = fontStyle 125 126 fun interceptFontSynthesis(fontSynthesis: FontSynthesis): FontSynthesis = fontSynthesis 127 128 companion object { 129 // NO-OP default interceptor 130 internal val Default: PlatformResolveInterceptor = object : PlatformResolveInterceptor {} 131 } 132 } 133 134 internal val GlobalTypefaceRequestCache = TypefaceRequestCache() 135 internal val GlobalAsyncTypefaceCache = AsyncTypefaceCache() 136 137 internal expect class PlatformFontFamilyTypefaceAdapter() : FontFamilyTypefaceAdapter { resolvenull138 override fun resolve( 139 typefaceRequest: TypefaceRequest, 140 platformFontLoader: PlatformFontLoader, 141 onAsyncCompletion: (TypefaceResult.Immutable) -> Unit, 142 createDefaultTypeface: (TypefaceRequest) -> Any 143 ): TypefaceResult? 144 } 145 146 internal data class TypefaceRequest( 147 val fontFamily: FontFamily?, 148 val fontWeight: FontWeight, 149 val fontStyle: FontStyle, 150 val fontSynthesis: FontSynthesis, 151 val resourceLoaderCacheKey: Any? 152 ) 153 154 internal sealed interface TypefaceResult : State<Any> { 155 val cacheable: Boolean 156 157 // Immutable results present as State, but don't trigger a read observer 158 class Immutable(override val value: Any, override val cacheable: Boolean = true) : 159 TypefaceResult 160 161 class Async(internal val current: AsyncFontListLoader) : TypefaceResult, State<Any> by current { 162 override val cacheable: Boolean 163 get() = current.cacheable 164 } 165 } 166 167 internal class TypefaceRequestCache { 168 internal val lock = makeSynchronizedObject() 169 // @GuardedBy("lock") 170 private val resultCache = LruCache<TypefaceRequest, TypefaceResult>(16) 171 runCachednull172 fun runCached( 173 typefaceRequest: TypefaceRequest, 174 resolveTypeface: ((TypefaceResult) -> Unit) -> TypefaceResult 175 ): State<Any> { 176 synchronized(lock) { 177 resultCache[typefaceRequest]?.let { 178 if (it.cacheable) { 179 return it 180 } else { 181 resultCache.remove(typefaceRequest) 182 } 183 } 184 } 185 // this is not run synchronized2 as it incurs expected file system reads. 186 // 187 // As a result, it is possible the same FontFamily resolution is started twice if this 188 // function is entered concurrently. This is explicitly allowed, to avoid creating a global 189 // lock here. 190 // 191 // This function must ensure that the final result is a valid cache in the presence of 192 // multiple entries. 193 // 194 // Necessary font load de-duping is the responsibility of actual font resolution mechanisms. 195 val currentTypefaceResult = 196 try { 197 resolveTypeface { finalResult -> 198 // may this after runCached returns, or immediately if the typeface is 199 // immediately 200 // available without dispatch 201 202 // this converts an async (state) result to an immutable (val) result to 203 // optimize 204 // future lookups 205 synchronized(lock) { 206 if (finalResult.cacheable) { 207 resultCache.put(typefaceRequest, finalResult) 208 } else { 209 resultCache.remove(typefaceRequest) 210 } 211 } 212 } 213 } catch (cause: Exception) { 214 throw IllegalStateException("Could not load font", cause) 215 } 216 synchronized(lock) { 217 // async result may have completed prior to this block entering, do not overwrite 218 // final results 219 if (resultCache[typefaceRequest] == null && currentTypefaceResult.cacheable) { 220 resultCache.put(typefaceRequest, currentTypefaceResult) 221 } 222 } 223 return currentTypefaceResult 224 } 225 preWarmCachenull226 fun preWarmCache( 227 typefaceRequests: List<TypefaceRequest>, 228 resolveTypeface: (TypefaceRequest) -> TypefaceResult 229 ) { 230 for (i in typefaceRequests.indices) { 231 val typeRequest = typefaceRequests[i] 232 233 val prior = synchronized(lock) { resultCache[typeRequest] } 234 if (prior != null) continue 235 236 val next = 237 try { 238 resolveTypeface(typeRequest) 239 } catch (cause: Exception) { 240 throw IllegalStateException("Could not load font", cause) 241 } 242 243 // only cache immutable, should not reach as FontListFontFamilyTypefaceAdapter already 244 // has async fonts in permanent cache 245 if (next is TypefaceResult.Async) continue 246 247 synchronized(lock) { resultCache.put(typeRequest, next) } 248 } 249 } 250 251 // @VisibleForTesting getnull252 internal fun get(typefaceRequest: TypefaceRequest) = 253 synchronized(lock) { resultCache.get(typefaceRequest) } 254 255 // @VisibleForTesting 256 internal val size: Int <lambda>null257 get() = synchronized(lock) { resultCache.size() } 258 } 259