DeviceFontFamilyNameFont.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.font

import android.content.Context
import android.graphics.Typeface
import androidx.compose.ui.text.ExperimentalTextApi

/**
 * Describes a system-installed font that may be present on some Android devices.
 *
 * You should assume this will not resolve on some devices and provide an appropriate fallback font.
 *
 * Family name lookup is device and platform-specific, and different OEMs may install different
 * fonts. All fonts described this way are considered [FontLoadingStrategy.OptionalLocal] and will
 * continue to the next font in the chain if they are not present on a device.
 *
 * Use this method to prefer locally pre-loaded system fonts when they are available. System fonts
 * are always more efficient to load than reading a font file, or downloadable fonts.
 *
 * A system installed font resolution will never trigger text reflow.
 *
 * This descriptor will trust the [weight] and [style] parameters as accurate. However, it is not
 * required that the loaded fonts actually support the requested weight and style and this may
 * trigger platform level font-synthesis of fake bold or fake italic during font resolution.
 *
 * This Font can not describe the system-installed [Typeface.DEFAULT]. All other system-installed
 * fonts are allowed.
 *
 * @param familyName Android system-installed font family name
 * @param weight weight to load
 * @param style style to load
 *
 * @throws IllegalArgumentException if familyName is empty
 */
@ExperimentalTextApi
fun Font(
    familyName: DeviceFontFamilyName,
    weight: FontWeight = FontWeight.Normal,
    style: FontStyle = FontStyle.Normal
): Font {
    return DeviceFontFamilyNameFont(familyName, weight, style)
}

/**
 * An Android system installed font family name as used by [Typeface.create].
 *
 * @see Typeface
 * @param name System fontFamilyName as passed to [Typeface.create]
 * @throws IllegalArgumentException if name is empty
 */
@ExperimentalTextApi
@JvmInline
value class DeviceFontFamilyName(val name: String) {
    init {
        require(name.isNotEmpty()) { "name may not be empty" }
    }
}

@ExperimentalTextApi
private class DeviceFontFamilyNameFont constructor(
    private val familyName: DeviceFontFamilyName,
    override val weight: FontWeight,
    override val style: FontStyle
) : AndroidFont(FontLoadingStrategy.OptionalLocal) {
    override val typefaceLoader: TypefaceLoader = NamedFontLoader

    val resolvedTypeface: Typeface? = lookupFont(familyName.name, weight, style)

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as DeviceFontFamilyNameFont

        if (familyName != other.familyName) return false
        if (weight != other.weight) return false
        if (style != other.style) return false

        return true
    }

    override fun hashCode(): Int {
        var result = familyName.hashCode()
        result = 31 * result + weight.hashCode()
        result = 31 * result + style.hashCode()
        return result
    }

    override fun toString(): String {
        return "Font(familyName=\"$familyName\", weight=$weight, style=$style)"
    }
}

/**
 * This basically checks to see if the family name isn't falling back to the Typeface loaded by
 * Typeface.DEFAULT.
 *
 * If so, we consider the family present and return the resulting typeface.
 */
private fun lookupFont(familyName: String, weight: FontWeight, style: FontStyle): Typeface? {
    if (familyName.isEmpty()) return null
    val typeface = createAndroidTypeface(familyName, weight, style)
    return typeface.takeIf {
        // Typeface may lookup missed results via either Typeface.DEFAULT or null, check both
        it != createAndroidTypeface(Typeface.DEFAULT, weight, style) &&
            it != createAndroidTypeface(null, weight, style)
    }
}

@ExperimentalTextApi
private object NamedFontLoader : AndroidFont.TypefaceLoader {
    override fun loadBlocking(context: Context, font: AndroidFont): Typeface? {
        return (font as? DeviceFontFamilyNameFont)?.resolvedTypeface
    }

    override suspend fun awaitLoad(context: Context, font: AndroidFont): Typeface? {
        throw UnsupportedOperationException("All preloaded fonts are optional local.")
    }
}