AndroidPreloadedFont.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.content.res.AssetManager
import android.graphics.Typeface
import android.graphics.fonts.FontVariationAxis
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastMap
import java.io.File

@OptIn(ExperimentalTextApi::class)
internal sealed class AndroidPreloadedFont @OptIn(ExperimentalTextApi::class) constructor(
    final override val weight: FontWeight,
    final override val style: FontStyle,
    variationSettings: FontVariation.Settings
) : AndroidFont(
    FontLoadingStrategy.Blocking,
    AndroidPreloadedFontTypefaceLoader,
    variationSettings
) {
    abstract val cacheKey: String?
    internal abstract fun doLoad(context: Context?): Typeface?

    private var didInitWithContext: Boolean = false
    // subclasses MUST initialize this by calling doLoad(null) - after overriding doLoad as final
    internal var typeface: Typeface? = null

    internal fun loadCached(context: Context): Typeface? {
        if (!didInitWithContext && typeface == null) {
            typeface = doLoad(context)
        }
        didInitWithContext = true
        return typeface
    }
}

private object AndroidPreloadedFontTypefaceLoader : AndroidFont.TypefaceLoader {
    override fun loadBlocking(context: Context, font: AndroidFont): Typeface? =
        (font as? AndroidPreloadedFont)?.loadCached(context)

    override suspend fun awaitLoad(context: Context, font: AndroidFont): Nothing {
        throw UnsupportedOperationException("All preloaded fonts are blocking.")
    }
}

@OptIn(ExperimentalTextApi::class) /* FontVariation.Settings */
internal class AndroidAssetFont constructor(
    val assetManager: AssetManager,
    val path: String,
    weight: FontWeight = FontWeight.Normal,
    style: FontStyle = FontStyle.Normal,
    variationSettings: FontVariation.Settings
) : AndroidPreloadedFont(weight, style, variationSettings) {

    override fun doLoad(context: Context?): Typeface? {
        return if (Build.VERSION.SDK_INT >= 26) {
            TypefaceBuilderCompat.createFromAssets(assetManager, path, context, variationSettings)
        } else {
            Typeface.createFromAsset(assetManager, path)
        }
    }

    init {
        typeface = doLoad(null)
    }

    override val cacheKey: String = "asset:$path"

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

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

        if (path != other.path) return false
        if (variationSettings != other.variationSettings) return false

        return true
    }

    override fun hashCode(): Int {
        var result = path.hashCode()
        result = 31 * result + variationSettings.hashCode()
        return result
    }
}

@OptIn(ExperimentalTextApi::class)
internal class AndroidFileFont constructor(
    val file: File,
    weight: FontWeight = FontWeight.Normal,
    style: FontStyle = FontStyle.Normal,
    variationSettings: FontVariation.Settings
) : AndroidPreloadedFont(weight, style, variationSettings) {

    override fun doLoad(context: Context?): Typeface? {
        return if (Build.VERSION.SDK_INT >= 26) {
            TypefaceBuilderCompat.createFromFile(file, context, variationSettings)
        } else {
            Typeface.createFromFile(file)
        }
    }

    init {
        typeface = doLoad(null)
    }

    override val cacheKey: String? = null
    override fun toString(): String {
        return "Font(file=$file, weight=$weight, style=$style)"
    }
}

@RequiresApi(26)
@OptIn(ExperimentalTextApi::class)
internal class AndroidFileDescriptorFont constructor(
    val fileDescriptor: ParcelFileDescriptor,
    weight: FontWeight = FontWeight.Normal,
    style: FontStyle = FontStyle.Normal,
    variationSettings: FontVariation.Settings
) : AndroidPreloadedFont(weight, style, variationSettings) {

    override fun doLoad(context: Context?): Typeface? {
        return if (Build.VERSION.SDK_INT >= 26) {
            TypefaceBuilderCompat.createFromFileDescriptor(
                fileDescriptor,
                context,
                variationSettings
            )
        } else {
            throw IllegalArgumentException("Cannot create font from file descriptor for SDK < 26")
        }
    }

    init {
        typeface = doLoad(null)
    }

    override val cacheKey: String? = null
    override fun toString(): String {
        return "Font(fileDescriptor=$fileDescriptor, weight=$weight, style=$style)"
    }
}

@RequiresApi(api = 26)
private object TypefaceBuilderCompat {
    @ExperimentalTextApi
    @DoNotInline
    fun createFromAssets(
        assetManager: AssetManager,
        path: String,
        context: Context?,
        variationSettings: FontVariation.Settings
    ): Typeface? {
        if (context == null) {
            return null
        }
        return Typeface.Builder(assetManager, path)
            .setFontVariationSettings(variationSettings.toVariationSettings(context))
            .build()
    }

    @ExperimentalTextApi
    @DoNotInline
    fun createFromFile(
        file: File,
        context: Context?,
        variationSettings: FontVariation.Settings
    ): Typeface? {
        if (context == null) {
            return null
        }
        return Typeface.Builder(file)
            .setFontVariationSettings(variationSettings.toVariationSettings(context))
            .build()
    }

    @ExperimentalTextApi
    @DoNotInline
    fun createFromFileDescriptor(
        fileDescriptor: ParcelFileDescriptor,
        context: Context?,
        variationSettings: FontVariation.Settings,
    ): Typeface? {
        if (context == null) {
            return null
        }
        return Typeface.Builder(fileDescriptor.fileDescriptor)
            .setFontVariationSettings(variationSettings.toVariationSettings(context))
            .build()
    }

    @RequiresApi(Build.VERSION_CODES.O)
    @ExperimentalTextApi
    private fun FontVariation.Settings.toVariationSettings(
        context: Context?
    ): Array<FontVariationAxis> {
        val density = if (context != null) {
            Density(context)
        } else if (!needsDensity) {
            // we don't need density, so make a fake one and be on with it
            Density(1f, 1f)
        } else {
            // cannot reach
            throw IllegalStateException("Required density, but not provided")
        }
        return settings.fastMap { setting ->
            FontVariationAxis(setting.axisName, setting.toVariationValue(density))
        }.toTypedArray()
    }
}