New emoji picker (#2395)

* Update to Emoji2

* Hopefully fix the emoji picker preference

* Switch to released Filemojicompat version

* Filemojicompat version as an own var

* Remove an unused import

* Small cleanup

* Correct onDisplayPreferenceDialog; test TuskyApplication

* Use TextViews instead of EmojiTextViews

* Recreate the Main Activity if the emoji pack is updated

* Enable coreLibraryDesugaring (for Java Streams); update Filemojicompat, downgrade Emoji2

* Update emoji font versions to 14

* Use FilemojiCompat 3.2.0-beta01

* Make ktLint happy again

* Remove coreLibraryDesugaring and a FIXME

* Use EmojiPickerPreference.get()

* Disable emoji pack import

* Update FilemojiCompat to Beta 2

* Update FilemojiCompat to Beta 3

* Update FilemojiCompat to Beta 3.2.0 final

* Update FilemojiCompat to 3.2.1
This commit is contained in:
Constantin A 2022-04-26 18:50:58 +02:00 committed by GitHub
parent 2fcd6fdc14
commit f15b3e61bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 109 additions and 782 deletions

View file

@ -96,6 +96,8 @@ ext.okhttpVersion = '4.9.3'
ext.glideVersion = '4.13.1'
ext.daggerVersion = '2.41'
ext.materialdrawerVersion = '8.4.5'
ext.emoji2_version = '1.1.0'
ext.filemojicompat_version = '3.2.1'
// if libraries are changed here, they should also be changed in LicenseActivity
dependencies {
@ -112,8 +114,9 @@ dependencies {
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
implementation "androidx.emoji:emoji:1.1.0"
implementation "androidx.emoji:emoji-appcompat:1.1.0"
implementation "androidx.emoji2:emoji2:$emoji2_version"
implementation "androidx.emoji2:emoji2-views:$emoji2_version"
implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
@ -170,7 +173,9 @@ dependencies {
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
implementation "de.c1710:filemojicompat:1.0.18"
implementation "de.c1710:filemojicompat-ui:$filemojicompat_version"
implementation "de.c1710:filemojicompat:$filemojicompat_version"
implementation "de.c1710:filemojicompat-defaults:$filemojicompat_version"
testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "org.robolectric:robolectric:4.4"

View file

@ -35,8 +35,8 @@ import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.EmojiCompat.InitCallback
import androidx.core.view.GravityCompat
import androidx.emoji2.text.EmojiCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
@ -114,6 +114,7 @@ import com.mikepenz.materialdrawer.util.updateBadge
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.launch
@ -150,13 +151,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private var accountLocked: Boolean = false
private val emojiInitCallback = object : InitCallback() {
override fun onInitialized() {
if (!isDestroyed) {
updateProfiles()
}
}
}
// We need to know if the emoji pack has been changed
private var selectedEmojiPack: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -271,11 +267,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Flush old media that was cached for sharing
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
}
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
}
override fun onResume() {
super.onResume()
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager)
val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
if (currentEmojiPack != selectedEmojiPack) {
Log.d(
TAG,
"onResume: EmojiPack has been changed from %s to %s"
.format(selectedEmojiPack, currentEmojiPack)
)
selectedEmojiPack = currentEmojiPack
recreate()
}
}
override fun onStart() {
super.onStart()
// For some reason the navigation drawer is opened when the activity is recreated
if (binding.mainDrawerLayout.isOpen) {
binding.mainDrawerLayout.closeDrawer(GravityCompat.START, false)
}
}
override fun onBackPressed() {
@ -333,11 +349,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
override fun onDestroy() {
super.onDestroy()
EmojiCompat.get().unregisterInitCallback(emojiInitCallback)
}
private fun forwardShare(intent: Intent) {
val composeIntent = Intent(this, ComposeActivity::class.java)
composeIntent.action = intent.action
@ -530,7 +541,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
)
}
EmojiCompat.get().registerInitCallback(emojiInitCallback)
}
override fun onSaveInstanceState(outState: Bundle) {
@ -800,7 +810,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))!!
ProfileDrawerItem().apply {
isSelected = acc.isActive

View file

@ -19,18 +19,18 @@ import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.ThemeUtils
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
import de.c1710.filemojicompat_ui.helpers.EmojiPreference
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import org.conscrypt.Conscrypt
import java.security.Security
@ -65,12 +65,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
// init the custom emoji fonts
val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0)
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
.getConfig(this)
.setReplaceAll(true)
EmojiCompat.init(emojiConfig)
// In this case, we want to have the emoji preferences merged with the other ones
// Copied from PreferenceManager.getDefaultSharedPreferenceName
EmojiPreference.sharedPreferenceName = packageName + "_preferences"
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
// init night mode
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)

View file

@ -19,7 +19,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.emoji.text.EmojiCompat
import androidx.emoji2.text.EmojiCompat
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemPollBinding

View file

@ -37,7 +37,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.updatePadding
import androidx.emoji.text.EmojiCompat
import androidx.emoji2.text.EmojiCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer

View file

@ -26,7 +26,7 @@ import androidx.core.view.OnReceiveContentListener
import androidx.core.view.ViewCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.emoji.widget.EmojiEditTextHelper
import androidx.emoji2.viewsintegration.EmojiEditTextHelper
class EditTextTyped @JvmOverloads constructor(
context: Context,

View file

@ -1,240 +0,0 @@
package com.keylesspalace.tusky.components.preference
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.RadioButton
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.SplashActivity
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.BLOBMOJI
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.NOTOEMOJI
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import okhttp3.OkHttpClient
import kotlin.system.exitProcess
/**
* This Preference lets the user select their preferred emoji font
*/
class EmojiPreference(
context: Context,
private val okHttpClient: OkHttpClient
) : Preference(context) {
private lateinit var selected: EmojiCompatFont
private lateinit var original: EmojiCompatFont
private val radioButtons = mutableListOf<RadioButton>()
private var updated = false
private var currentNeedsUpdate = false
private val downloadDisposables = MutableList<Disposable?>(FONTS.size) { null }
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
super.onAttachedToHierarchy(preferenceManager)
// Find out which font is currently active
selected = EmojiCompatFont.byId(
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
)
// We'll use this later to determine if anything has changed
original = selected
summary = selected.getDisplay(context)
}
override fun onClick() {
val binding = DialogEmojicompatBinding.inflate(LayoutInflater.from(context))
setupItem(BLOBMOJI, binding.itemBlobmoji)
setupItem(TWEMOJI, binding.itemTwemoji)
setupItem(NOTOEMOJI, binding.itemNotoemoji)
setupItem(SYSTEM_DEFAULT, binding.itemNomoji)
AlertDialog.Builder(context)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
// Initialize all the views
binding.emojiName.text = font.getDisplay(context)
binding.emojiCaption.setText(font.caption)
binding.emojiThumbnail.setImageResource(font.img)
// There needs to be a list of all the radio buttons in order to uncheck them when one is selected
radioButtons.add(binding.emojiRadioButton)
updateItem(font, binding)
// Set actions
binding.emojiDownload.setOnClickListener { startDownload(font, binding) }
binding.emojiDownloadCancel.setOnClickListener { cancelDownload(font, binding) }
binding.emojiRadioButton.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
binding.root.setOnClickListener {
select(font, binding.emojiRadioButton)
}
}
private fun startDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
// Switch to downloading style
binding.emojiDownload.hide()
binding.emojiCaption.visibility = View.INVISIBLE
binding.emojiProgress.show()
binding.emojiProgress.progress = 0
binding.emojiDownloadCancel.show()
font.downloadFontFile(context, okHttpClient)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
if (progress >= 0) {
binding.emojiProgress.isIndeterminate = false
val max = binding.emojiProgress.max.toFloat()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.emojiProgress.setProgress((max * progress).toInt(), true)
} else {
binding.emojiProgress.progress = (max * progress).toInt()
}
} else {
binding.emojiProgress.isIndeterminate = true
}
},
{
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
updateItem(font, binding)
},
{
finishDownload(font, binding)
}
).also { downloadDisposables[font.id] = it }
}
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
font.deleteDownloadedFile(context)
downloadDisposables[font.id]?.dispose()
downloadDisposables[font.id] = null
updateItem(font, binding)
}
private fun finishDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
select(font, binding.emojiRadioButton)
updateItem(font, binding)
// Set the flag to restart the app (because an update has been downloaded)
if (selected === original && currentNeedsUpdate) {
updated = true
currentNeedsUpdate = false
}
}
/**
* Select a font both visually and logically
*
* @param font The font to be selected
* @param radio The radio button associated with it's visual item
*/
private fun select(font: EmojiCompatFont, radio: RadioButton) {
selected = font
radioButtons.forEach { radioButton ->
radioButton.isChecked = radioButton == radio
}
}
/**
* Called when a "consistent" state is reached, i.e. it's not downloading the font
*
* @param font The font to be displayed
* @param binding The ItemEmojiPrefBinding to show the item in
*/
private fun updateItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
// There's no download going on
binding.emojiProgress.hide()
binding.emojiDownloadCancel.hide()
binding.emojiCaption.show()
if (font.isDownloaded(context)) {
// Make it selectable
binding.emojiDownload.hide()
binding.emojiRadioButton.show()
binding.root.isClickable = true
} else {
// Make it downloadable
binding.emojiDownload.show()
binding.emojiRadioButton.hide()
binding.root.isClickable = false
}
// Select it if necessary
if (font === selected) {
binding.emojiRadioButton.isChecked = true
// Update available
if (!font.isDownloaded(context)) {
currentNeedsUpdate = true
}
} else {
binding.emojiRadioButton.isChecked = false
}
}
private fun saveSelectedFont() {
val index = selected.id
Log.i(TAG, "saveSelectedFont: Font ID: $index")
PreferenceManager
.getDefaultSharedPreferences(context)
.edit()
.putInt(key, index)
.apply()
summary = selected.getDisplay(context)
}
/**
* User clicked ok -> save the selected font and offer to restart the app if something changed
*/
private fun onDialogOk() {
saveSelectedFont()
if (selected !== original || updated) {
AlertDialog.Builder(context)
.setTitle(R.string.restart_required)
.setMessage(R.string.restart_emoji)
.setNegativeButton(R.string.later, null)
.setPositiveButton(R.string.restart) { _, _ ->
// Restart the app
// From https://stackoverflow.com/a/17166729/5070653
val launchIntent = Intent(context, SplashActivity::class.java)
val mPendingIntent = PendingIntent.getActivity(
context,
0x1f973, // This is the codepoint of the party face emoji :D
launchIntent,
NotificationHelper.pendingIntentFlags(false)
)
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mgr.set(
AlarmManager.RTC,
System.currentTimeMillis() + 100,
mPendingIntent
)
exitProcess(0)
}.show()
}
}
companion object {
private const val TAG = "EmojiPreference"
}
}

View file

@ -38,14 +38,11 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
import okhttp3.OkHttpClient
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
import javax.inject.Inject
class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var okhttpclient: OkHttpClient
@Inject
lateinit var accountManager: AccountManager
@ -65,11 +62,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
}
emojiPreference(okhttpclient) {
setDefaultValue("system_default")
setIcon(R.drawable.ic_emoji_24dp)
key = PrefKeys.EMOJI
setSummary(R.string.system_default)
emojiPreference(requireActivity()) {
setTitle(R.string.emoji_style)
icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied)
}
@ -300,6 +293,12 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
override fun onDisplayPreferenceDialog(preference: Preference) {
if (!EmojiPickerPreference.onDisplayPreferenceDialog(this, preference)) {
super.onDisplayPreferenceDialog(preference)
}
}
companion object {
fun newInstance(): PreferencesFragment {
return PreferencesFragment()

View file

@ -1,7 +1,9 @@
package com.keylesspalace.tusky.settings
import android.content.Context
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.annotation.StringRes
import androidx.lifecycle.LifecycleOwner
import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@ -10,8 +12,7 @@ import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreference
import com.keylesspalace.tusky.components.preference.EmojiPreference
import okhttp3.OkHttpClient
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
class PreferenceParent(
val context: Context,
@ -32,8 +33,9 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit):
return pref
}
inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference {
val pref = EmojiPreference(context, okHttpClient)
inline fun <A> PreferenceParent.emojiPreference(activity: A, builder: EmojiPickerPreference.() -> Unit): EmojiPickerPreference
where A : Context, A : ActivityResultRegistryOwner, A : LifecycleOwner {
val pref = EmojiPickerPreference.get(activity)
builder(pref)
addPref(pref)
return pref

View file

@ -1,364 +0,0 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.util.Log
import android.util.Pair
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import com.keylesspalace.tusky.R
import de.c1710.filemojicompat.FileEmojiCompatConfig
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.ObservableEmitter
import io.reactivex.rxjava3.schedulers.Schedulers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.internal.toLongOrDefault
import okio.Source
import okio.buffer
import okio.sink
import java.io.EOFException
import java.io.File
import java.io.FilenameFilter
import java.io.IOException
import kotlin.math.max
/**
* This class bundles information about an emoji font as well as many convenient actions.
*/
class EmojiCompatFont(
val name: String,
private val display: String,
@StringRes val caption: Int,
@DrawableRes val img: Int,
val url: String,
// The version is stored as a String in the x.xx.xx format (to be able to compare versions)
val version: String
) {
private val versionCode = getVersionCode(version)
// A list of all available font files and whether they are older than the current version or not
// They are ordered by their version codes in ascending order
private var existingFontFileCache: List<Pair<File, List<Int>>>? = null
val id: Int
get() = FONTS.indexOf(this)
fun getDisplay(context: Context): String {
return if (this !== SYSTEM_DEFAULT) display else context.getString(R.string.system_default)
}
/**
* This method will return the actual font file (regardless of its existence) for
* the current version (not necessarily the latest!).
*
* @return The font (TTF) file or null if called on SYSTEM_FONT
*/
private fun getFontFile(context: Context): File? {
return if (this !== SYSTEM_DEFAULT) {
val directory = File(context.getExternalFilesDir(null), DIRECTORY)
File(directory, "$name$version.ttf")
} else {
null
}
}
fun getConfig(context: Context): FileEmojiCompatConfig {
return FileEmojiCompatConfig(context, getLatestFontFile(context))
}
fun isDownloaded(context: Context): Boolean {
return this === SYSTEM_DEFAULT || getFontFile(context)?.exists() == true || fontFileExists(context)
}
/**
* Checks whether there is already a font version that satisfies the current version, i.e. it
* has a higher or equal version code.
*
* @param context The Context
* @return Whether there is a font file with a higher or equal version code to the current
*/
private fun fontFileExists(context: Context): Boolean {
val existingFontFiles = getExistingFontFiles(context)
return if (existingFontFiles.isNotEmpty()) {
compareVersions(existingFontFiles.last().second, versionCode) >= 0
} else {
false
}
}
/**
* Deletes any older version of a font
*
* @param context The current Context
*/
private fun deleteOldVersions(context: Context) {
val existingFontFiles = getExistingFontFiles(context)
Log.d(TAG, "deleting old versions...")
Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size))
for (fileExists in existingFontFiles) {
if (compareVersions(fileExists.second, versionCode) < 0) {
val file = fileExists.first
// Uses side effects!
Log.d(
TAG,
String.format(
"Deleted %s successfully: %s", file.absolutePath,
file.delete()
)
)
}
}
}
/**
* Loads all font files that are inside the files directory into an ArrayList with the information
* on whether they are older than the currently available version or not.
*
* @param context The Context
*/
private fun getExistingFontFiles(context: Context): List<Pair<File, List<Int>>> {
// Only load it once
existingFontFileCache?.let {
return it
}
// If we call this on the system default font, just return nothing...
if (this === SYSTEM_DEFAULT) {
existingFontFileCache = emptyList()
return emptyList()
}
val directory = File(context.getExternalFilesDir(null), DIRECTORY)
// It will search for old versions using a regex that matches the font's name plus
// (if present) a version code. No version code will be regarded as version 0.
val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern()
val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") }
val foundFontFiles = directory.listFiles(ttfFilter).orEmpty()
Log.d(
TAG,
String.format(
"loadExistingFontFiles: %d other font files found",
foundFontFiles.size
)
)
return foundFontFiles.map { file ->
val matcher = fontRegex.matcher(file.name)
val versionCode = if (matcher.matches()) {
val version = matcher.group(1)
getVersionCode(version)
} else {
listOf(0)
}
Pair(file, versionCode)
}.sortedWith { a, b ->
compareVersions(a.second, b.second)
}.also {
existingFontFileCache = it
}
}
/**
* Returns the current or latest version of this font file (if there is any)
*
* @param context The Context
* @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font.
*/
private fun getLatestFontFile(context: Context): File? {
val current = getFontFile(context)
if (current != null && current.exists()) return current
val existingFontFiles = getExistingFontFiles(context)
return existingFontFiles.firstOrNull()?.first
}
private fun getVersionCode(version: String?): List<Int> {
if (version == null) return listOf(0)
return version.split(".").map {
it.toIntOrNull() ?: 0
}
}
fun downloadFontFile(
context: Context,
okHttpClient: OkHttpClient
): Observable<Float> {
return Observable.create { emitter: ObservableEmitter<Float> ->
// It is possible (and very likely) that the file does not exist yet
val downloadFile = getFontFile(context)!!
if (!downloadFile.exists()) {
downloadFile.parentFile?.mkdirs()
downloadFile.createNewFile()
}
val request = Request.Builder().url(url)
.build()
val sink = downloadFile.sink().buffer()
var source: Source? = null
try {
// Download!
val response = okHttpClient.newCall(request).execute()
val responseBody = response.body
if (response.isSuccessful && responseBody != null) {
val size = response.length()
var progress = 0f
source = responseBody.source()
try {
while (!emitter.isDisposed) {
sink.write(source, CHUNK_SIZE)
progress += CHUNK_SIZE.toFloat()
if (size > 0) {
emitter.onNext(progress / size)
} else {
emitter.onNext(-1f)
}
}
} catch (ex: EOFException) {
/*
This means we've finished downloading the file since sink.write
will throw an EOFException when the file to be read is empty.
*/
}
} else {
Log.e(TAG, "Downloading $url failed. Status code: ${response.code}")
emitter.tryOnError(Exception())
}
} catch (ex: IOException) {
Log.e(TAG, "Downloading $url failed.", ex)
downloadFile.deleteIfExists()
emitter.tryOnError(ex)
} finally {
source?.close()
sink.close()
if (emitter.isDisposed) {
downloadFile.deleteIfExists()
} else {
deleteOldVersions(context)
emitter.onComplete()
}
}
}
.subscribeOn(Schedulers.io())
}
/**
* Deletes the downloaded file, if it exists. Should be called when a download gets cancelled.
*/
fun deleteDownloadedFile(context: Context) {
getFontFile(context)?.deleteIfExists()
}
override fun toString(): String {
return display
}
companion object {
private const val TAG = "EmojiCompatFont"
/**
* This String represents the sub-directory the fonts are stored in.
*/
private const val DIRECTORY = "emoji"
private const val CHUNK_SIZE = 4096L
// The system font gets some special behavior...
val SYSTEM_DEFAULT = EmojiCompatFont(
"system-default",
"System Default",
R.string.caption_systememoji,
R.drawable.ic_emoji_34dp,
"",
"0"
)
val BLOBMOJI = EmojiCompatFont(
"Blobmoji",
"Blobmoji",
R.string.caption_blobmoji,
R.drawable.ic_blobmoji,
"https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
"14.0.1"
)
val TWEMOJI = EmojiCompatFont(
"Twemoji",
"Twemoji",
R.string.caption_twemoji,
R.drawable.ic_twemoji,
"https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
"14.0.0"
)
val NOTOEMOJI = EmojiCompatFont(
"NotoEmoji",
"Noto Emoji",
R.string.caption_notoemoji,
R.drawable.ic_notoemoji,
"https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
"14.0.0"
)
/**
* This array stores all available EmojiCompat fonts.
* References to them can simply be saved by saving their indices
*/
val FONTS = listOf(SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI)
/**
* Returns the Emoji font associated with this ID
*
* @param id the ID of this font
* @return the corresponding font. Will default to SYSTEM_DEFAULT if not in range.
*/
fun byId(id: Int): EmojiCompatFont = FONTS.getOrElse(id) { SYSTEM_DEFAULT }
/**
* Compares two version codes to each other
*
* @param versionA The first version
* @param versionB The second version
* @return -1 if versionA < versionB, 1 if versionA > versionB and 0 otherwise
*/
@VisibleForTesting
fun compareVersions(versionA: List<Int>, versionB: List<Int>): Int {
val len = max(versionB.size, versionA.size)
for (i in 0 until len) {
val vA = versionA.getOrElse(i) { 0 }
val vB = versionB.getOrElse(i) { 0 }
// It needs to be decided on the next level
if (vA == vB) continue
// Okay, is version B newer or version A?
return vA.compareTo(vB)
}
// The versions are equal
return 0
}
/**
* This method is needed because when transparent compression is used OkHttp reports
* [ResponseBody.contentLength] as -1. We try to get the header which server sent
* us manually here.
*
* @see [OkHttp issue 259](https://github.com/square/okhttp/issues/259)
*/
private fun Response.length(): Long {
networkResponse?.let {
val header = it.header("Content-Length") ?: return -1
return header.toLongOrDefault(-1)
}
// In case it's a fully cached response
return body?.contentLength() ?: -1
}
private fun File.deleteIfExists() {
if (exists() && !delete()) {
Log.e(TAG, "Could not delete file $this")
}
}
}
}

View file

@ -112,7 +112,7 @@
app:layout_constraintStart_toStartOf="@id/guideAvatar"
app:layout_constraintTop_toTopOf="@+id/accountFollowButton" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/accountDisplayNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -215,7 +215,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountNoteTextInputLayout" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/accountNoteTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -257,7 +257,7 @@
app:layout_constraintTop_toBottomOf="@id/accountRemoveView"
tools:visibility="visible">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/accountMovedText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -283,7 +283,7 @@
app:layout_constraintTop_toBottomOf="@id/accountMovedText"
tools:src="@drawable/avatar_default" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/accountMovedDisplayName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -72,7 +72,7 @@
tools:text="Reply to @username"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/composeReplyContentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -94,7 +94,7 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.emoji.widget.EmojiEditText
<androidx.emoji2.widget.EmojiEditText
android:id="@+id/composeContentWarningField"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="16dp">
<include
android:id="@+id/item_blobmoji"
layout="@layout/item_emoji_pref" />
<include
android:id="@+id/item_twemoji"
layout="@layout/item_emoji_pref" />
<include
android:id="@+id/item_notoemoji"
layout="@layout/item_emoji_pref" />
<include
android:id="@+id/item_nomoji"
layout="@layout/item_emoji_pref" />
<TextView
android:id="@+id/emoji_download_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.1"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="8dp"
android:text="@string/download_fonts"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>

View file

@ -32,7 +32,7 @@
tools:src="#000"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/account_display_name"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -7,7 +7,7 @@
android:paddingTop="4dp">
<!-- 30% width for the field name, 70% for the value -->
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/accountFieldName"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -21,7 +21,7 @@
app:layout_constraintWidth_percent=".3"
tools:text="Field title" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/accountFieldValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -22,7 +22,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -38,7 +38,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/blocked_user_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -11,7 +11,7 @@
android:paddingStart="12dp"
android:paddingEnd="14dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/conversation_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -79,7 +79,7 @@
tools:src="#000"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -122,7 +122,7 @@
app:layout_constraintTop_toTopOf="@id/status_display_name"
tools:text="13:37" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_content_warning_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -157,7 +157,7 @@
tools:text="@string/post_content_warning_show_more"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_content"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -28,7 +28,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/contentWarning"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -42,7 +42,7 @@
app:layout_constraintTop_toBottomOf="@id/draftSendingInfo"
tools:text="Some content warning" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -17,7 +17,7 @@
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<androidx.emoji.widget.EmojiEditText
<androidx.emoji2.widget.EmojiEditText
android:id="@+id/accountFieldName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -26,7 +26,7 @@
android:textColorHint="?android:attr/textColorTertiary"
android:textSize="?attr/status_text_medium" />
<androidx.emoji.widget.EmojiEditText
<androidx.emoji2.widget.EmojiEditText
android:id="@+id/accountFieldValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -9,7 +9,7 @@
android:paddingRight="14dp"
android:paddingBottom="10dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/notification_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -41,7 +41,7 @@
app:layout_constraintTop_toTopOf="@id/notification_display_name"
tools:src="@drawable/avatar_default" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/notification_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -8,7 +8,7 @@
android:paddingRight="16dp"
android:paddingBottom="10dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/notificationTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -36,7 +36,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationTextView" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/displayNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -51,7 +51,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/muted_user_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_poll_option_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -14,7 +14,7 @@
android:orientation="vertical"
app:layout_constraintGuide_begin="8dp" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/statusContentWarningDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -49,7 +49,7 @@
tools:text="@string/post_content_warning_show_more"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/statusContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -237,7 +237,7 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_poll_option_result_0"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -257,7 +257,7 @@
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
tools:text="40%" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_poll_option_result_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -277,7 +277,7 @@
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_0"
tools:text="10%" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_poll_option_result_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -297,7 +297,7 @@
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_1"
tools:text="20%" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_poll_option_result_3"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -5,7 +5,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -12,7 +12,7 @@
android:paddingLeft="14dp"
android:paddingRight="14dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -55,7 +55,7 @@
tools:src="#000"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -101,7 +101,7 @@
app:layout_constraintTop_toTopOf="@id/status_display_name"
tools:text="13:37" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_content_warning_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -141,7 +141,7 @@
tools:text="@string/post_content_warning_show_more"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -191,7 +191,7 @@
android:paddingRight="6dp"
android:paddingBottom="6dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -202,7 +202,7 @@
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/card_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -36,7 +36,7 @@
tools:src="#000"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -79,7 +79,7 @@
app:layout_constraintTop_toBottomOf="@id/status_display_name"
tools:text="\@ConnyDuck\@mastodon.social" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_content_warning_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -113,7 +113,7 @@
app:layout_constraintTop_toBottomOf="@+id/status_content_warning_description"
tools:text="@string/post_content_warning_show_more" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -162,7 +162,7 @@
android:paddingRight="6dp"
android:paddingBottom="6dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -173,7 +173,7 @@
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/card_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -8,7 +8,7 @@
android:paddingLeft="14dp"
android:paddingRight="14dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/notification_top_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -32,7 +32,7 @@
android:layout_toEndOf="@+id/notification_status_avatar"
android:paddingBottom="4dp">
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/status_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -71,7 +71,7 @@
</RelativeLayout>
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/notification_content_warning_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -102,7 +102,7 @@
style="@style/TuskyButton.Outlined"
android:textSize="?attr/status_text_medium" />
<androidx.emoji.widget.EmojiTextView
<TextView
android:id="@+id/notification_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -18,16 +18,16 @@ package com.keylesspalace.tusky
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import androidx.emoji.text.EmojiCompat
import com.keylesspalace.tusky.util.LocaleManager
import de.c1710.filemojicompat.FileEmojiCompatConfig
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
// override TuskyApplication for Robolectric tests, only initialize the necessary stuff
class TuskyApplication : Application() {
override fun onCreate() {
super.onCreate()
EmojiCompat.init(FileEmojiCompatConfig(this, ""))
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this))
}
override fun attachBaseContext(base: Context) {

View file

@ -1,47 +0,0 @@
package com.keylesspalace.tusky.util
import org.junit.Assert.assertEquals
import org.junit.Test
class EmojiCompatFontTest {
@Test
fun testCompareVersions() {
assertEquals(
-1,
EmojiCompatFont.compareVersions(
listOf(0),
listOf(1, 2, 3)
)
)
assertEquals(
1,
EmojiCompatFont.compareVersions(
listOf(1, 2, 3),
listOf(0, 0, 0)
)
)
assertEquals(
-1,
EmojiCompatFont.compareVersions(
listOf(1, 0, 1),
listOf(1, 1, 0)
)
)
assertEquals(
0,
EmojiCompatFont.compareVersions(
listOf(4, 5, 6),
listOf(4, 5, 6)
)
)
assertEquals(
0,
EmojiCompatFont.compareVersions(
listOf(0, 0),
listOf(0)
)
)
}
}