From 6fb6092a6bd6f484a573d08594c54036ce70e176 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 6 Apr 2022 13:12:28 -0300 Subject: [PATCH] Implement a cache for faster typeface resolution. --- .../org/thoughtcrime/securesms/fonts/Fonts.kt | 9 +-- .../securesms/fonts/TypefaceCache.kt | 72 +++++++++++++++++++ .../securesms/jobs/FontDownloaderJob.kt | 5 +- .../v2/text/TextStoryPostCreationViewModel.kt | 9 +-- .../securesms/stories/StoryTextPostModel.kt | 9 +++ .../securesms/stories/StoryTextPostView.kt | 13 ---- .../text/StoryTextPostPreviewFragment.kt | 9 ++- .../viewer/text/StoryTextPostRepository.kt | 17 +++++ .../stories/viewer/text/StoryTextPostState.kt | 4 +- .../viewer/text/StoryTextPostViewModel.kt | 21 ++++++ 10 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/fonts/TypefaceCache.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/fonts/Fonts.kt b/app/src/main/java/org/thoughtcrime/securesms/fonts/Fonts.kt index fd956b5d0d..1800ea799a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/fonts/Fonts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/fonts/Fonts.kt @@ -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 = 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, guessedScript: SupportedScript): SupportedScript { + fun getSupportedScript(locales: List, guessedScript: SupportedScript): SupportedScript { if (guessedScript != SupportedScript.UNKNOWN && guessedScript != SupportedScript.UNKNOWN_CJK) { return guessedScript } else if (guessedScript == SupportedScript.UNKNOWN_CJK) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/fonts/TypefaceCache.kt b/app/src/main/java/org/thoughtcrime/securesms/fonts/TypefaceCache.kt new file mode 100644 index 0000000000..284cc2b32a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/fonts/TypefaceCache.kt @@ -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()) + + /** + * 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 { + 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 { 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 { + 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 + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/FontDownloaderJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/FontDownloaderJob.kt index 33d24d43c1..0175d27b69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/FontDownloaderJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/FontDownloaderJob.kt @@ -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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt index 31ed832e4e..de4af0ce38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt index 28a0949515..36bd392d38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt @@ -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 { 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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt index 1f56e0a7e6..e7f5c841ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt index 2ebc2340bb..eab9326c15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt @@ -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().mediaNotAvailable() } } + + if (state.typeface != null) { + storyTextPostView.setTypeface(state.typeface) + } + + if (state.typeface != null && state.loadState == StoryTextPostState.LoadState.LOADED) { + loadPreview(storyTextThumb, storyTextPostView) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostRepository.kt index 0aeabe844c..d2774f629f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostRepository.kt @@ -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 { @@ -11,4 +18,14 @@ class StoryTextPostRepository { SignalDatabase.mms.getMessageRecord(recordId) as MmsMessageRecord }.subscribeOn(Schedulers.io()) } + + fun getTypeface(recordId: Long): Single { + 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) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostState.kt index fc38cade46..0bf2e52f81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostState.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostViewModel.kt index 02f895a925..eb71c589ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostViewModel.kt @@ -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 = 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()) {