Implement a cache for faster typeface resolution.

fork-5.53.8
Alex Hart 2022-04-06 13:12:28 -03:00 zatwierdzone przez Cody Henthorne
rodzic 46bb64ad24
commit 6fb6092a6b
10 zmienionych plików z 139 dodań i 29 usunięć

Wyświetl plik

@ -8,7 +8,6 @@ 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
@ -52,12 +51,12 @@ object Fonts {
*
* @param context An application context
* @param font The desired font
* @param guessedScript The script likely being used based on text content
* @param supportedScript 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, font: TextFont, guessedScript: SupportedScript = SupportedScript.UNKNOWN): FontResult {
fun resolveFont(context: Context, font: TextFont, supportedScript: SupportedScript): FontResult {
ThreadUtil.assertNotMainThread()
synchronized(this) {
val errorFallback = FontResult.Immediate(Typeface.create(font.fallbackFamily, font.fallbackStyle))
@ -70,8 +69,6 @@ object Fonts {
Log.d(TAG, "Loaded manifest.")
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.")
@ -253,7 +250,7 @@ object Fonts {
}
}
private fun getSupportedScript(locales: List<Locale>, guessedScript: SupportedScript): SupportedScript {
fun getSupportedScript(locales: List<Locale>, guessedScript: SupportedScript): SupportedScript {
if (guessedScript != SupportedScript.UNKNOWN && guessedScript != SupportedScript.UNKNOWN_CJK) {
return guessedScript
} else if (guessedScript == SupportedScript.UNKNOWN_CJK) {

Wyświetl plik

@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.fonts
import android.content.Context
import android.graphics.Typeface
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.util.FutureTaskListener
import org.thoughtcrime.securesms.util.LocaleUtil
import java.util.Collections
import java.util.concurrent.ExecutionException
/**
* In-Memory Typeface cache
*/
object TypefaceCache {
private val cache = Collections.synchronizedMap(mutableMapOf<CacheKey, Typeface>())
/**
* Warms the typeface-cache with all fonts of a given script.
*/
fun warm(context: Context, script: SupportedScript) {
val appContext = context.applicationContext
TextFont.values().forEach {
get(appContext, it, script).subscribe()
}
}
/**
* Grabs the font and caches it on the fly.
*/
fun get(context: Context, font: TextFont, guessedScript: SupportedScript = SupportedScript.UNKNOWN): Single<Typeface> {
val supportedScript = Fonts.getSupportedScript(LocaleUtil.getLocaleDefaults(), guessedScript)
val cacheKey = CacheKey(supportedScript, font)
val cachedValue = cache[cacheKey]
val appContext = context.applicationContext
if (cachedValue != null) {
return Single.just(cachedValue)
} else {
return Single.create<Typeface> { emitter ->
when (val result = Fonts.resolveFont(appContext, font, supportedScript)) {
is Fonts.FontResult.Immediate -> {
cache[cacheKey] = result.typeface
emitter.onSuccess(result.typeface)
}
is Fonts.FontResult.Async -> {
val listener = object : FutureTaskListener<Typeface> {
override fun onSuccess(typeface: Typeface) {
cache[cacheKey] = typeface
emitter.onSuccess(typeface)
}
override fun onFailure(exception: ExecutionException) {
emitter.onSuccess(result.placeholder)
}
}
result.future.addListener(listener)
emitter.setCancellable {
result.future.removeListener(listener)
}
}
}
}.subscribeOn(Schedulers.io())
}
}
private data class CacheKey(
val script: SupportedScript,
val font: TextFont
)
}

Wyświetl plik

@ -3,11 +3,13 @@ package org.thoughtcrime.securesms.jobs
import android.graphics.Typeface
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.fonts.Fonts
import org.thoughtcrime.securesms.fonts.SupportedScript
import org.thoughtcrime.securesms.fonts.TextFont
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 org.thoughtcrime.securesms.util.LocaleUtil
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
@ -40,8 +42,9 @@ class FontDownloaderJob private constructor(parameters: Parameters) : BaseJob(pa
override fun onFailure() = Unit
override fun onRun() {
val script = Fonts.getSupportedScript(LocaleUtil.getLocaleDefaults(), SupportedScript.UNKNOWN)
val asyncResults = TextFont.values()
.map { Fonts.resolveFont(context, it) }
.map { Fonts.resolveFont(context, it, script) }
.filterIsInstance(Fonts.FontResult.Async::class.java)
if (asyncResults.isEmpty()) {

Wyświetl plik

@ -17,6 +17,7 @@ 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.fonts.TypefaceCache
import org.thoughtcrime.securesms.util.FutureTaskListener
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.concurrent.ExecutionException
@ -44,13 +45,7 @@ class TextStoryPostCreationViewModel : ViewModel() {
Observable.combineLatest(textFontSubject, scriptGuess, ::Pair)
.observeOn(Schedulers.io())
.distinctUntilChanged()
.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)
}
}
.switchMapSingle { (textFont, script) -> TypefaceCache.get(ApplicationDependencies.getApplication(), textFont, script) }
.subscribeOn(Schedulers.io())
.subscribe {
internalTypeface.postValue(it)

Wyświetl plik

@ -18,6 +18,9 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.fonts.TextFont
import org.thoughtcrime.securesms.fonts.TextToScript
import org.thoughtcrime.securesms.fonts.TypefaceCache
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
@ -80,7 +83,13 @@ data class StoryTextPostModel(
override fun decode(source: StoryTextPostModel, width: Int, height: Int, options: Options): Resource<Bitmap> {
val message = SignalDatabase.mmsSms.getMessageFor(source.storySentAtMillis, source.storyAuthor)
val view = StoryTextPostView(ApplicationDependencies.getApplication())
val typeface = TypefaceCache.get(
ApplicationDependencies.getApplication(),
TextFont.fromStyle(source.storyTextPost.style),
TextToScript.guessScript(source.storyTextPost.body)
).blockingGet()
view.setTypeface(typeface)
view.bindFromStoryTextPost(source.storyTextPost)
view.bindLinkPreview((message as? MmsMessageRecord)?.linkPreviews?.firstOrNull())

Wyświetl plik

@ -12,13 +12,10 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible
import com.google.android.material.imageview.ShapeableImageView
import org.signal.core.util.concurrent.SimpleTask
import org.thoughtcrime.securesms.R
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
@ -154,16 +151,6 @@ class StoryTextPostView @JvmOverloads constructor(
setTextBackgroundColor(storyTextPost.textBackgroundColor)
setTextGravity(TextAlignment.CENTER)
SimpleTask.run(
{
when (val fontResult = Fonts.resolveFont(context, font, TextToScript.guessScript(storyTextPost.body))) {
is Fonts.FontResult.Immediate -> fontResult.typeface
is Fonts.FontResult.Async -> fontResult.future.get()
}
},
{ typeface -> setTypeface(typeface) }
)
hideCloseButton()
postAdjustLinkPreviewTranslationY()

Wyświetl plik

@ -66,13 +66,20 @@ class StoryTextPostPreviewFragment : Fragment(R.layout.stories_text_post_preview
} else {
storyTextPostView.setLinkPreviewClickListener(null)
}
loadPreview(storyTextThumb, storyTextPostView)
}
StoryTextPostState.LoadState.FAILED -> {
requireActivity().supportStartPostponedEnterTransition()
requireListener<MediaPreviewFragment.Events>().mediaNotAvailable()
}
}
if (state.typeface != null) {
storyTextPostView.setTypeface(state.typeface)
}
if (state.typeface != null && state.loadState == StoryTextPostState.LoadState.LOADED) {
loadPreview(storyTextThumb, storyTextPostView)
}
}
}

Wyświetl plik

@ -1,9 +1,16 @@
package org.thoughtcrime.securesms.stories.viewer.text
import android.graphics.Typeface
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.fonts.TextFont
import org.thoughtcrime.securesms.fonts.TextToScript
import org.thoughtcrime.securesms.fonts.TypefaceCache
import org.thoughtcrime.securesms.util.Base64
class StoryTextPostRepository {
fun getRecord(recordId: Long): Single<MmsMessageRecord> {
@ -11,4 +18,14 @@ class StoryTextPostRepository {
SignalDatabase.mms.getMessageRecord(recordId) as MmsMessageRecord
}.subscribeOn(Schedulers.io())
}
fun getTypeface(recordId: Long): Single<Typeface> {
return getRecord(recordId).flatMap {
val model = StoryTextPost.parseFrom(Base64.decode(it.body))
val textFont = TextFont.fromStyle(model.style)
val script = TextToScript.guessScript(model.body)
TypefaceCache.get(ApplicationDependencies.getApplication(), textFont, script)
}
}
}

Wyświetl plik

@ -1,12 +1,14 @@
package org.thoughtcrime.securesms.stories.viewer.text
import android.graphics.Typeface
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.linkpreview.LinkPreview
data class StoryTextPostState(
val storyTextPost: StoryTextPost? = null,
val linkPreview: LinkPreview? = null,
val loadState: LoadState = LoadState.INIT
val loadState: LoadState = LoadState.INIT,
val typeface: Typeface? = null
) {
enum class LoadState {
INIT,

Wyświetl plik

@ -1,23 +1,44 @@
package org.thoughtcrime.securesms.stories.viewer.text
import android.graphics.Typeface
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.livedata.Store
class StoryTextPostViewModel(recordId: Long, repository: StoryTextPostRepository) : ViewModel() {
companion object {
private val TAG = Log.tag(StoryTextPostViewModel::class.java)
}
private val store = Store(StoryTextPostState())
private val disposables = CompositeDisposable()
val state: LiveData<StoryTextPostState> = store.stateLiveData
init {
disposables += repository.getTypeface(recordId)
.subscribeBy(
onSuccess = { typeface ->
store.update {
it.copy(typeface = typeface)
}
},
onError = { error ->
Log.w(TAG, "Failed to get typeface. Rendering with default.", error)
store.update {
it.copy(typeface = Typeface.DEFAULT)
}
}
)
disposables += repository.getRecord(recordId)
.map {
if (it.body.isNotEmpty()) {