mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-05 08:47:10 +00:00
Add defaults for script/text font pairings and guessing of script based on body contents.
Co-authored-by: Alex Hart <alex@signal.org>
This commit is contained in:
@@ -28,25 +28,25 @@ data class FontManifest(
|
||||
* @param chineseSimplified Hans Script Fonts
|
||||
*/
|
||||
data class FontScripts(
|
||||
@JsonProperty("latin-extended") val latinExtended: FontScript,
|
||||
@JsonProperty("cyrillic-extended") val cyrillicExtended: FontScript,
|
||||
val devanagari: FontScript,
|
||||
@JsonProperty("chinese-traditional-hk") val chineseTraditionalHk: FontScript,
|
||||
@JsonProperty("chinese-traditional") val chineseTraditional: FontScript,
|
||||
@JsonProperty("chinese-simplified") val chineseSimplified: FontScript,
|
||||
val arabic: FontScript,
|
||||
val japanese: FontScript,
|
||||
@JsonProperty("latin-extended") val latinExtended: FontScript?,
|
||||
@JsonProperty("cyrillic-extended") val cyrillicExtended: FontScript?,
|
||||
val devanagari: FontScript?,
|
||||
@JsonProperty("chinese-traditional-hk") val chineseTraditionalHk: FontScript?,
|
||||
@JsonProperty("chinese-traditional") val chineseTraditional: FontScript?,
|
||||
@JsonProperty("chinese-simplified") val chineseSimplified: FontScript?,
|
||||
val arabic: FontScript?,
|
||||
val japanese: FontScript?,
|
||||
)
|
||||
|
||||
/**
|
||||
* A collection of fonts for a specific script
|
||||
*/
|
||||
data class FontScript(
|
||||
val regular: String,
|
||||
val bold: String,
|
||||
val serif: String,
|
||||
val script: String,
|
||||
val condensed: String
|
||||
val regular: String?,
|
||||
val bold: String?,
|
||||
val serif: String?,
|
||||
val script: String?,
|
||||
val condensed: String?
|
||||
)
|
||||
|
||||
companion object {
|
||||
@@ -76,7 +76,7 @@ data class FontManifest(
|
||||
objectMapper.readValue(it, FontManifest::class.java)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to load manifest from disk")
|
||||
Log.w(TAG, "Failed to load manifest from disk", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,41 +2,44 @@ package org.thoughtcrime.securesms.fonts
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import org.signal.imageeditor.core.Renderer
|
||||
import org.signal.imageeditor.core.RendererContext
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener
|
||||
import org.thoughtcrime.securesms.util.LocaleUtil
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
/**
|
||||
* RenderContext TypefaceProvider that provides typefaces using TextFont.
|
||||
*/
|
||||
object FontTypefaceProvider : RendererContext.TypefaceProvider {
|
||||
override fun getSelectedTypeface(context: Context, renderer: Renderer, invalidate: RendererContext.Invalidate): Typeface {
|
||||
return getTypeface()
|
||||
// TODO [cody] Need to rework Fonts.kt to not hit network on main, reverting to old typeface for now
|
||||
// return when (val fontResult = Fonts.resolveFont(context, Locale.getDefault(), TextFont.BOLD)) {
|
||||
// is Fonts.FontResult.Immediate -> fontResult.typeface
|
||||
// is Fonts.FontResult.Async -> {
|
||||
// fontResult.future.addListener(object : FutureTaskListener<Typeface> {
|
||||
// override fun onSuccess(result: Typeface?) {
|
||||
// invalidate.onInvalidate(renderer)
|
||||
// }
|
||||
//
|
||||
// override fun onFailure(exception: ExecutionException?) = Unit
|
||||
// })
|
||||
//
|
||||
// fontResult.placeholder
|
||||
// }
|
||||
// }
|
||||
}
|
||||
class FontTypefaceProvider : RendererContext.TypefaceProvider {
|
||||
|
||||
private fun getTypeface(): Typeface {
|
||||
return if (Build.VERSION.SDK_INT < 26) {
|
||||
Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
} else {
|
||||
Typeface.Builder("")
|
||||
.setFallback("sans-serif")
|
||||
.setWeight(900)
|
||||
.build()
|
||||
private var cachedTypeface: Typeface? = null
|
||||
private var cachedLocale: Locale? = null
|
||||
|
||||
override fun getSelectedTypeface(context: Context, renderer: Renderer, invalidate: RendererContext.Invalidate): Typeface {
|
||||
val typeface = cachedTypeface
|
||||
if (typeface != null && cachedLocale == LocaleUtil.getFirstLocale()) {
|
||||
return typeface
|
||||
}
|
||||
|
||||
return when (val fontResult = Fonts.resolveFont(context, TextFont.BOLD)) {
|
||||
is Fonts.FontResult.Immediate -> {
|
||||
cachedTypeface = fontResult.typeface
|
||||
cachedLocale = LocaleUtil.getFirstLocale()
|
||||
fontResult.typeface
|
||||
}
|
||||
is Fonts.FontResult.Async -> {
|
||||
fontResult.future.addListener(object : FutureTaskListener<Typeface> {
|
||||
override fun onSuccess(result: Typeface?) {
|
||||
invalidate.onInvalidate(renderer)
|
||||
}
|
||||
|
||||
override fun onFailure(exception: ExecutionException?) = Unit
|
||||
})
|
||||
|
||||
fontResult.placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.s3.S3
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask
|
||||
import org.thoughtcrime.securesms.util.LocaleUtil
|
||||
import java.io.File
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
@@ -46,15 +47,16 @@ object Fonts {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve a Typeface for the given font / locale combination
|
||||
* Attempts to retrieve a Typeface for the given font / guessed script and default locales combination
|
||||
*
|
||||
* @param context An application context
|
||||
* @param locale The locale the content will be displayed in
|
||||
* @param font The desired font
|
||||
* @param guessedScript The script likely being used based on text content
|
||||
*
|
||||
* @return a FontResult that represents either a Typeface or a task retrieving a Typeface.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun resolveFont(context: Context, locale: Locale, font: TextFont): FontResult {
|
||||
fun resolveFont(context: Context, font: TextFont, guessedScript: SupportedScript = SupportedScript.UNKNOWN): FontResult {
|
||||
synchronized(this) {
|
||||
val errorFallback = FontResult.Immediate(Typeface.create(font.fallbackFamily, font.fallbackStyle))
|
||||
val version = FontVersion.get(context)
|
||||
@@ -66,11 +68,21 @@ object Fonts {
|
||||
|
||||
Log.d(TAG, "Loaded manifest.")
|
||||
|
||||
val fontScript = resolveScriptNameFromLocale(locale, manifest) ?: return errorFallback
|
||||
val localeDefaults: List<Locale> = LocaleUtil.getLocaleDefaults()
|
||||
val supportedScript: SupportedScript = getSupportedScript(localeDefaults, guessedScript)
|
||||
val fontScript = resolveFontScriptFromScriptName(supportedScript, manifest)
|
||||
if (fontScript == null) {
|
||||
Log.d(TAG, "Manifest does not have an entry for $supportedScript. Using default.")
|
||||
return FontResult.Immediate(getDefaultFontForScriptAndStyle(supportedScript, font))
|
||||
}
|
||||
|
||||
Log.d(TAG, "Loaded script for locale.")
|
||||
|
||||
val fontNetworkPath = getScriptPath(font, fontScript)
|
||||
if (fontNetworkPath == null) {
|
||||
Log.d(TAG, "Manifest does not contain a network path for $supportedScript. Using default.")
|
||||
return FontResult.Immediate(getDefaultFontForScriptAndStyle(supportedScript, font))
|
||||
}
|
||||
|
||||
val fontLocalPath = FontFileMap.getNameOnDisk(context, version, fontNetworkPath)
|
||||
|
||||
@@ -80,7 +92,7 @@ object Fonts {
|
||||
}
|
||||
|
||||
val fontDownloadKey = FontDownloadKey(
|
||||
version, locale, font
|
||||
version, supportedScript, font
|
||||
)
|
||||
|
||||
val taskInProgress = taskCache[fontDownloadKey]
|
||||
@@ -93,7 +105,7 @@ object Fonts {
|
||||
} else {
|
||||
Log.d(TAG, "Could not find a task in progress. Returning new async.")
|
||||
val newTask = ListenableFutureTask {
|
||||
val newLocalPath = downloadFont(context, locale, font, version, manifest)
|
||||
val newLocalPath = downloadFont(context, supportedScript, font, version, manifest)
|
||||
Log.d(TAG, "Finished download, $newLocalPath")
|
||||
|
||||
val typeface = newLocalPath?.let { loadFontIntoTypeface(context, version, it) } ?: errorFallback.typeface
|
||||
@@ -112,6 +124,69 @@ object Fonts {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDefaultFontForScriptAndStyle(supportedScript: SupportedScript, font: TextFont): Typeface {
|
||||
return when (supportedScript) {
|
||||
SupportedScript.CYRILLIC -> {
|
||||
when (font) {
|
||||
TextFont.REGULAR -> Typeface.SANS_SERIF
|
||||
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
TextFont.SERIF -> Typeface.SERIF
|
||||
TextFont.SCRIPT -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SERIF, "semibold", TypefaceHelper.Weight.SEMI_BOLD)
|
||||
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "light", TypefaceHelper.Weight.LIGHT)
|
||||
}
|
||||
}
|
||||
SupportedScript.DEVANAGARI -> {
|
||||
when (font) {
|
||||
TextFont.REGULAR -> Typeface.SANS_SERIF
|
||||
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
TextFont.SERIF -> Typeface.SANS_SERIF
|
||||
TextFont.SCRIPT -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "light", TypefaceHelper.Weight.LIGHT)
|
||||
}
|
||||
}
|
||||
SupportedScript.CHINESE_TRADITIONAL_HK,
|
||||
SupportedScript.CHINESE_TRADITIONAL,
|
||||
SupportedScript.CHINESE_SIMPLIFIED,
|
||||
SupportedScript.UNKNOWN_CJK -> {
|
||||
when (font) {
|
||||
TextFont.REGULAR -> Typeface.SANS_SERIF
|
||||
TextFont.BOLD -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "semibold", TypefaceHelper.Weight.SEMI_BOLD)
|
||||
TextFont.SERIF -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "thin", TypefaceHelper.Weight.THIN)
|
||||
TextFont.SCRIPT -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "light", TypefaceHelper.Weight.LIGHT)
|
||||
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "demilight", TypefaceHelper.Weight.DEMI_LIGHT)
|
||||
}
|
||||
}
|
||||
SupportedScript.ARABIC -> {
|
||||
when (font) {
|
||||
TextFont.REGULAR -> Typeface.SANS_SERIF
|
||||
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
TextFont.SERIF -> Typeface.SERIF
|
||||
TextFont.SCRIPT -> Typeface.create(Typeface.SERIF, Typeface.BOLD)
|
||||
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "black", TypefaceHelper.Weight.BLACK)
|
||||
}
|
||||
}
|
||||
SupportedScript.JAPANESE -> {
|
||||
when (font) {
|
||||
TextFont.REGULAR -> Typeface.SANS_SERIF
|
||||
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
TextFont.SERIF -> Typeface.SERIF
|
||||
TextFont.SCRIPT -> Typeface.create(Typeface.SERIF, Typeface.BOLD)
|
||||
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "medium", TypefaceHelper.Weight.MEDIUM)
|
||||
}
|
||||
}
|
||||
SupportedScript.LATIN,
|
||||
SupportedScript.UNKNOWN -> {
|
||||
when (font) {
|
||||
TextFont.REGULAR -> Typeface.SANS_SERIF
|
||||
TextFont.BOLD -> Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
TextFont.SERIF -> Typeface.SERIF
|
||||
TextFont.SCRIPT -> Typeface.create(Typeface.SERIF, Typeface.BOLD)
|
||||
TextFont.CONDENSED -> TypefaceHelper.typefaceFor(TypefaceHelper.Family.SANS_SERIF, "black", TypefaceHelper.Weight.BLACK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun loadFontIntoTypeface(context: Context, fontVersion: FontVersion, fontLocalPath: String): Typeface? {
|
||||
return try {
|
||||
@@ -146,9 +221,13 @@ object Fonts {
|
||||
* Downloads the given font file from S3
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun downloadFont(context: Context, locale: Locale, font: TextFont, fontVersion: FontVersion, fontManifest: FontManifest): String? {
|
||||
val script: FontManifest.FontScript = resolveScriptNameFromLocale(locale, fontManifest) ?: return null
|
||||
val path = getScriptPath(font, script)
|
||||
private fun downloadFont(context: Context, supportedScript: SupportedScript?, font: TextFont, fontVersion: FontVersion, fontManifest: FontManifest): String? {
|
||||
if (supportedScript == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val script: FontManifest.FontScript = resolveFontScriptFromScriptName(supportedScript, fontManifest) ?: return null
|
||||
val path = getScriptPath(font, script) ?: return null
|
||||
val networkPath = "$BASE_STATIC_BUCKET_URL/${fontVersion.id}/$path"
|
||||
val localUUID = UUID.randomUUID().toString()
|
||||
val localPath = "${fontVersion.path}/" + localUUID
|
||||
@@ -162,7 +241,7 @@ object Fonts {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getScriptPath(font: TextFont, script: FontManifest.FontScript): String {
|
||||
private fun getScriptPath(font: TextFont, script: FontManifest.FontScript): String? {
|
||||
return when (font) {
|
||||
TextFont.REGULAR -> script.regular
|
||||
TextFont.BOLD -> script.bold
|
||||
@@ -172,22 +251,62 @@ object Fonts {
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveScriptNameFromLocale(locale: Locale, fontManifest: FontManifest): FontManifest.FontScript? {
|
||||
val fontScript: FontManifest.FontScript = when (ScriptUtil.getScript(locale).apply { Log.d(TAG, "Getting Script for $this") }) {
|
||||
ScriptUtil.LATIN -> fontManifest.scripts.latinExtended
|
||||
ScriptUtil.ARABIC -> fontManifest.scripts.arabic
|
||||
ScriptUtil.CHINESE_SIMPLIFIED -> fontManifest.scripts.chineseSimplified
|
||||
ScriptUtil.CHINESE_TRADITIONAL -> fontManifest.scripts.chineseTraditional
|
||||
ScriptUtil.CYRILLIC -> fontManifest.scripts.cyrillicExtended
|
||||
ScriptUtil.DEVANAGARI -> fontManifest.scripts.devanagari
|
||||
ScriptUtil.JAPANESE -> fontManifest.scripts.japanese
|
||||
else -> return null
|
||||
private fun getSupportedScript(locales: List<Locale>, guessedScript: SupportedScript): SupportedScript {
|
||||
if (guessedScript != SupportedScript.UNKNOWN && guessedScript != SupportedScript.UNKNOWN_CJK) {
|
||||
return guessedScript
|
||||
} else if (guessedScript == SupportedScript.UNKNOWN_CJK) {
|
||||
val likelyScript: SupportedScript? = locales.mapNotNull {
|
||||
try {
|
||||
when (it.isO3Country) {
|
||||
"HKG" -> SupportedScript.CHINESE_TRADITIONAL_HK
|
||||
"CHN" -> SupportedScript.CHINESE_SIMPLIFIED
|
||||
"TWN" -> SupportedScript.CHINESE_TRADITIONAL
|
||||
"JPN" -> SupportedScript.JAPANESE
|
||||
else -> null
|
||||
}
|
||||
} catch (e: java.util.MissingResourceException) {
|
||||
Log.w(TAG, "Unable to get ISO-3 country code for: $it")
|
||||
null
|
||||
}
|
||||
}.firstOrNull()
|
||||
|
||||
if (likelyScript != null) {
|
||||
return likelyScript
|
||||
}
|
||||
}
|
||||
|
||||
return if (fontScript == fontManifest.scripts.chineseSimplified && locale.isO3Country == "HKG") {
|
||||
fontManifest.scripts.chineseTraditionalHk
|
||||
val locale = locales.first()
|
||||
val supportedScript: SupportedScript = when (ScriptUtil.getScript(locale).also { Log.d(TAG, "Getting Script for $it") }) {
|
||||
ScriptUtil.LATIN -> SupportedScript.LATIN
|
||||
ScriptUtil.ARABIC -> SupportedScript.ARABIC
|
||||
ScriptUtil.CHINESE_SIMPLIFIED -> SupportedScript.CHINESE_SIMPLIFIED
|
||||
ScriptUtil.CHINESE_TRADITIONAL -> SupportedScript.CHINESE_TRADITIONAL
|
||||
ScriptUtil.CYRILLIC -> SupportedScript.CYRILLIC
|
||||
ScriptUtil.DEVANAGARI -> SupportedScript.DEVANAGARI
|
||||
ScriptUtil.JAPANESE -> SupportedScript.JAPANESE
|
||||
else -> SupportedScript.UNKNOWN
|
||||
}
|
||||
|
||||
return if (supportedScript == SupportedScript.CHINESE_SIMPLIFIED && locale.isO3Country == "HKG") {
|
||||
SupportedScript.CHINESE_TRADITIONAL_HK
|
||||
} else {
|
||||
fontScript
|
||||
supportedScript
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveFontScriptFromScriptName(supportedScript: SupportedScript?, fontManifest: FontManifest): FontManifest.FontScript? {
|
||||
return when (supportedScript.also { Log.d(TAG, "Getting Script for $it") }) {
|
||||
SupportedScript.LATIN -> fontManifest.scripts.latinExtended
|
||||
SupportedScript.ARABIC -> fontManifest.scripts.arabic
|
||||
SupportedScript.CHINESE_SIMPLIFIED -> fontManifest.scripts.chineseSimplified
|
||||
SupportedScript.CHINESE_TRADITIONAL -> fontManifest.scripts.chineseTraditional
|
||||
SupportedScript.CYRILLIC -> fontManifest.scripts.cyrillicExtended
|
||||
SupportedScript.DEVANAGARI -> fontManifest.scripts.devanagari
|
||||
SupportedScript.JAPANESE -> fontManifest.scripts.japanese
|
||||
SupportedScript.CHINESE_TRADITIONAL_HK -> fontManifest.scripts.chineseTraditionalHk
|
||||
SupportedScript.UNKNOWN_CJK -> null
|
||||
SupportedScript.UNKNOWN -> null
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +320,7 @@ object Fonts {
|
||||
|
||||
private data class FontDownloadKey(
|
||||
val version: FontVersion,
|
||||
val locale: Locale,
|
||||
val script: SupportedScript,
|
||||
val font: TextFont
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.fonts
|
||||
|
||||
/**
|
||||
* Scripts with font support for Stories
|
||||
*/
|
||||
enum class SupportedScript {
|
||||
LATIN,
|
||||
CYRILLIC,
|
||||
DEVANAGARI,
|
||||
CHINESE_TRADITIONAL_HK,
|
||||
CHINESE_TRADITIONAL,
|
||||
CHINESE_SIMPLIFIED,
|
||||
UNKNOWN_CJK,
|
||||
ARABIC,
|
||||
JAPANESE,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.fonts
|
||||
|
||||
/**
|
||||
* Attempt to guess the script based on the unicode characters used. The script
|
||||
* with the most use in a string will be picked. Tie goes to [SupportedScript.LATIN].
|
||||
* Unicode does not cleanly separate Hong Kong and Chinese character ranges so some
|
||||
* other means must be used to distinguish it (e.g., Locale).
|
||||
*/
|
||||
object TextToScript {
|
||||
|
||||
private val LATIN_RANGES: List<IntRange> = listOf(0x0000..0x024F, 0x1E00..0x1EFF, 0x2C60..0x2C7F, 0xA720..0xA7FF, 0xAB30..0xAB6F)
|
||||
private val CYRILLIC_RANGES: List<IntRange> = listOf(0x0400..0x04FF, 0x0500..0x052F, 0x1C80..0x1C8F, 0x2DE0..0x2DFF, 0xA640..0xA69F)
|
||||
private val DEVANAGARI_RANGES: List<IntRange> = listOf(0x0900..0x097F, 0xA8E0..0xA8FF, 0x30A0..0x30FF)
|
||||
private val CJK_RANGES: List<IntRange> = listOf(0x31C0..0x31EF, 0x3300..0x33FF, 0x4E00..0x9FFF, 0xF900..0xFAFF, 0xFE30..0xFE4F, 0x20000..0x2EBEF, 0x2F800..0x2FA1F)
|
||||
private val CJK_JAPANESE_RANGES: List<IntRange> = listOf(0x3040..0x309F, 0x30A0..0x30FF, 0x3190..0x319F)
|
||||
private val ARABIC_RANGES: List<IntRange> = listOf(0x0600..0x06FF, 0x0750..0x077F, 0x0870..0x089F, 0x08A0..0x08FF)
|
||||
|
||||
private val allRanges = mapOf(
|
||||
SupportedScript.LATIN to LATIN_RANGES,
|
||||
SupportedScript.CYRILLIC to CYRILLIC_RANGES,
|
||||
SupportedScript.DEVANAGARI to DEVANAGARI_RANGES,
|
||||
SupportedScript.UNKNOWN_CJK to CJK_RANGES,
|
||||
SupportedScript.JAPANESE to CJK_JAPANESE_RANGES,
|
||||
SupportedScript.ARABIC to ARABIC_RANGES
|
||||
)
|
||||
|
||||
fun guessScript(text: CharSequence): SupportedScript {
|
||||
val scriptCounts: MutableMap<SupportedScript, Int> = SupportedScript.values().associate { it to 0 }.toMutableMap()
|
||||
val input = text.toString()
|
||||
|
||||
for (i in 0 until input.codePointCount(0, input.length)) {
|
||||
val codePoint = input.codePointAt(i)
|
||||
for ((script, ranges) in allRanges) {
|
||||
if (ranges.contains(codePoint)) {
|
||||
scriptCounts[script] = scriptCounts[script]!! + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val most: SupportedScript = scriptCounts.maxByOrNull { it.value }?.key ?: SupportedScript.UNKNOWN
|
||||
|
||||
return if (most == SupportedScript.UNKNOWN_CJK && scriptCounts[SupportedScript.JAPANESE]!! > 0) {
|
||||
SupportedScript.JAPANESE
|
||||
} else {
|
||||
most
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<IntRange>.contains(x: Int): Boolean = any { x in it }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.thoughtcrime.securesms.fonts
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
|
||||
/**
|
||||
* API independent, best-effort way of creating [Typeface]s of various family/weight.
|
||||
*/
|
||||
object TypefaceHelper {
|
||||
fun typefaceFor(family: Family, weightName: String, weight: Weight): Typeface {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= 28 -> Typeface.create(
|
||||
Typeface.create(family.familyName, Typeface.NORMAL),
|
||||
weight.value,
|
||||
false
|
||||
)
|
||||
Build.VERSION.SDK_INT >= 21 -> Typeface.create("${family.familyName}-$weightName", Typeface.NORMAL)
|
||||
else -> Typeface.create(family.familyName, if (weight.value > Weight.MEDIUM.value) Typeface.BOLD else Typeface.NORMAL)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Family(val familyName: String) {
|
||||
SANS_SERIF("sans-serif"),
|
||||
SERIF("serif")
|
||||
}
|
||||
|
||||
enum class Weight(val value: Int) {
|
||||
THIN(100),
|
||||
EXTRA_LIGHT(200),
|
||||
DEMI_LIGHT(200),
|
||||
LIGHT(300),
|
||||
NORMAL(400),
|
||||
MEDIUM(500),
|
||||
SEMI_BOLD(600),
|
||||
BOLD(700),
|
||||
EXTRA_BOLD(800),
|
||||
BLACK(900),
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -41,10 +40,8 @@ class FontDownloaderJob private constructor(parameters: Parameters) : BaseJob(pa
|
||||
override fun onFailure() = Unit
|
||||
|
||||
override fun onRun() {
|
||||
val locale = Locale.getDefault()
|
||||
|
||||
val asyncResults = TextFont.values()
|
||||
.map { Fonts.resolveFont(context, locale, it) }
|
||||
.map { Fonts.resolveFont(context, it) }
|
||||
.filterIsInstance(Fonts.FontResult.Async::class.java)
|
||||
|
||||
if (asyncResults.isEmpty()) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
|
||||
public @NonNull Media transform(@NonNull Context context, @NonNull Media media) {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
Bitmap bitmap = modelToRender.render(context, size, FontTypefaceProvider.INSTANCE);
|
||||
Bitmap bitmap = modelToRender.render(context, size, new FontTypefaceProvider());
|
||||
try {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
|
||||
|
||||
@@ -16,15 +16,16 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.fonts.Fonts
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.fonts.TextToScript
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
class TextStoryPostCreationViewModel : ViewModel() {
|
||||
|
||||
private val store = Store(TextStoryPostCreationState())
|
||||
private val textFontSubject: Subject<TextFont> = BehaviorSubject.create()
|
||||
private val temporaryBodySubject: Subject<String> = BehaviorSubject.createDefault("")
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val internalThumbnail = MutableLiveData<Bitmap>()
|
||||
@@ -38,14 +39,16 @@ class TextStoryPostCreationViewModel : ViewModel() {
|
||||
init {
|
||||
textFontSubject.onNext(store.state.textFont)
|
||||
|
||||
textFontSubject
|
||||
val scriptGuess = temporaryBodySubject.observeOn(Schedulers.io()).map { TextToScript.guessScript(it) }
|
||||
|
||||
Observable.combineLatest(textFontSubject, scriptGuess, ::Pair)
|
||||
.observeOn(Schedulers.io())
|
||||
.distinctUntilChanged()
|
||||
.map { Fonts.resolveFont(ApplicationDependencies.getApplication(), Locale.getDefault(), it) }
|
||||
.switchMap {
|
||||
when (it) {
|
||||
is Fonts.FontResult.Async -> asyncFontEmitter(it)
|
||||
is Fonts.FontResult.Immediate -> Observable.just(it.typeface)
|
||||
.map { (textFont, script) -> Fonts.resolveFont(ApplicationDependencies.getApplication(), textFont, script) }
|
||||
.switchMap { result ->
|
||||
when (result) {
|
||||
is Fonts.FontResult.Async -> asyncFontEmitter(result)
|
||||
is Fonts.FontResult.Immediate -> Observable.just(result.typeface)
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -142,6 +145,10 @@ class TextStoryPostCreationViewModel : ViewModel() {
|
||||
store.update { it.copy(linkPreviewUri = url) }
|
||||
}
|
||||
|
||||
fun setTemporaryBody(temporaryBody: String) {
|
||||
temporaryBodySubject.onNext(temporaryBody)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(TextStoryPostCreationViewModel::class.java)
|
||||
private const val TEXT_STORY_INSTANCE_STATE = "text.story.instance.state"
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.appcompat.widget.AppCompatSeekBar
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.transition.TransitionManager
|
||||
@@ -112,6 +113,9 @@ class TextStoryPostTextEntryFragment : KeyboardEntryDialogFragment(
|
||||
input.doOnTextChanged { _, _, _, _ ->
|
||||
presentHint()
|
||||
}
|
||||
input.doAfterTextChanged { text ->
|
||||
viewModel.setTemporaryBody(text?.toString() ?: "")
|
||||
}
|
||||
input.setText(viewModel.getBody())
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
imageEditorHud = view.findViewById(R.id.scribble_hud);
|
||||
imageEditorView = view.findViewById(R.id.image_editor_view);
|
||||
|
||||
imageEditorView.setTypefaceProvider(FontTypefaceProvider.INSTANCE);
|
||||
imageEditorView.setTypefaceProvider(new FontTypefaceProvider());
|
||||
|
||||
int width = getResources().getDisplayMetrics().widthPixels;
|
||||
int height = (int) ((16 / 9f) * width);
|
||||
@@ -555,7 +555,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
FaceDetector detector = new AndroidFaceDetector();
|
||||
|
||||
Point size = model.getOutputSizeMaxWidth(1000);
|
||||
Bitmap render = model.render(ApplicationDependencies.getApplication(), size, FontTypefaceProvider.INSTANCE);
|
||||
Bitmap render = model.render(ApplicationDependencies.getApplication(), size, new FontTypefaceProvider());
|
||||
try {
|
||||
return new FaceDetectionResult(detector.detect(render), new Point(render.getWidth(), render.getHeight()), inverseCropPosition);
|
||||
} finally {
|
||||
@@ -769,7 +769,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
@WorkerThread
|
||||
public @NonNull Uri renderToSingleUseBlob() {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
Bitmap image = imageEditorView.getModel().render(requireContext(), FontTypefaceProvider.INSTANCE);
|
||||
Bitmap image = imageEditorView.getModel().render(requireContext(), new FontTypefaceProvider());
|
||||
|
||||
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
image.recycle();
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.fonts.Fonts
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.fonts.TextToScript
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextAlignment
|
||||
@@ -25,7 +26,6 @@ import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryScale
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryTextWatcher
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
|
||||
@@ -153,15 +153,10 @@ class StoryTextPostView @JvmOverloads constructor(
|
||||
setTextBackgroundColor(storyTextPost.textBackgroundColor)
|
||||
setTextGravity(TextAlignment.CENTER)
|
||||
|
||||
SimpleTask.run(
|
||||
{
|
||||
when (val fontResult = Fonts.resolveFont(context, Locale.getDefault(), font)) {
|
||||
is Fonts.FontResult.Immediate -> fontResult.typeface
|
||||
is Fonts.FontResult.Async -> fontResult.future.get()
|
||||
}
|
||||
},
|
||||
{ typeface -> setTypeface(typeface) }
|
||||
)
|
||||
when (val fontResult = Fonts.resolveFont(context, font, TextToScript.guessScript(storyTextPost.body))) {
|
||||
is Fonts.FontResult.Immediate -> setTypeface(fontResult.typeface)
|
||||
is Fonts.FontResult.Async -> setTypeface(fontResult.future.get())
|
||||
}
|
||||
|
||||
hideCloseButton()
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.LanguageString
|
||||
import java.util.Locale
|
||||
|
||||
object LocaleUtil {
|
||||
|
||||
fun getFirstLocale(): Locale {
|
||||
return getLocaleDefaults().firstOrNull() ?: Locale.getDefault()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user priority list of locales supported on the device, with the locale set via Signal settings
|
||||
* as highest priority over system settings.
|
||||
*/
|
||||
fun getLocaleDefaults(): List<Locale> {
|
||||
val locales: MutableList<Locale> = mutableListOf()
|
||||
val signalLocale: Locale? = LanguageString.parseLocale(SignalStore.settings().language)
|
||||
val localeList: LocaleListCompat = LocaleListCompat.getDefault()
|
||||
|
||||
if (signalLocale != null) {
|
||||
locales += signalLocale
|
||||
}
|
||||
|
||||
for (index in 0 until localeList.size()) {
|
||||
locales += localeList.get(index)
|
||||
}
|
||||
|
||||
return locales
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ final class WallpaperCropViewModel extends ViewModel {
|
||||
{
|
||||
SignalExecutors.BOUNDED.execute(
|
||||
() -> {
|
||||
Bitmap bitmap = model.render(context, size, FontTypefaceProvider.INSTANCE);
|
||||
Bitmap bitmap = model.render(context, size, new FontTypefaceProvider());
|
||||
try {
|
||||
ChatWallpaper chatWallpaper = repository.setWallPaper(BitmapUtil.toWebPByteArray(bitmap));
|
||||
callback.onComplete(chatWallpaper);
|
||||
|
||||
Reference in New Issue
Block a user