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:
Cody Henthorne
2022-03-30 12:00:29 -04:00
parent 116e711f1a
commit 284a6ae667
15 changed files with 436 additions and 92 deletions

View File

@@ -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
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
)
}

View File

@@ -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
}

View File

@@ -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 }
}

View File

@@ -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),
}
}

View File

@@ -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()) {

View File

@@ -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);

View File

@@ -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"

View File

@@ -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())
}

View File

@@ -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();

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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);