kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement a cache for faster typeface resolution.
rodzic
46bb64ad24
commit
6fb6092a6b
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()) {
|
||||
|
|
Ładowanie…
Reference in New Issue