From f15b3e61bbcd89d89af0307a0993540eba333352 Mon Sep 17 00:00:00 2001 From: Constantin A <10349490+C1710@users.noreply.github.com> Date: Tue, 26 Apr 2022 18:50:58 +0200 Subject: [PATCH] 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 --- app/build.gradle | 11 +- .../com/keylesspalace/tusky/MainActivity.kt | 42 +- .../keylesspalace/tusky/TuskyApplication.kt | 16 +- .../tusky/adapter/PollAdapter.kt | 2 +- .../components/account/AccountActivity.kt | 2 +- .../components/compose/view/EditTextTyped.kt | 2 +- .../components/preference/EmojiPreference.kt | 240 ------------ .../preference/PreferencesFragment.kt | 17 +- .../tusky/settings/SettingsDSL.kt | 10 +- .../tusky/util/EmojiCompatFont.kt | 364 ------------------ app/src/main/res/layout/activity_account.xml | 8 +- app/src/main/res/layout/activity_compose.xml | 4 +- .../main/res/layout/dialog_emojicompat.xml | 36 -- app/src/main/res/layout/item_account.xml | 2 +- .../main/res/layout/item_account_field.xml | 4 +- app/src/main/res/layout/item_announcement.xml | 2 +- .../res/layout/item_autocomplete_account.xml | 2 +- app/src/main/res/layout/item_blocked_user.xml | 2 +- app/src/main/res/layout/item_conversation.xml | 8 +- app/src/main/res/layout/item_draft.xml | 4 +- app/src/main/res/layout/item_edit_field.xml | 4 +- app/src/main/res/layout/item_follow.xml | 4 +- .../main/res/layout/item_follow_request.xml | 4 +- app/src/main/res/layout/item_muted_user.xml | 2 +- app/src/main/res/layout/item_poll.xml | 2 +- .../main/res/layout/item_report_status.xml | 12 +- .../main/res/layout/item_scheduled_status.xml | 2 +- app/src/main/res/layout/item_status.xml | 12 +- .../main/res/layout/item_status_detailed.xml | 10 +- .../res/layout/item_status_notification.xml | 8 +- .../keylesspalace/tusky/TuskyApplication.kt | 6 +- .../tusky/util/EmojiCompatFontTest.kt | 47 --- 32 files changed, 109 insertions(+), 782 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt delete mode 100644 app/src/main/res/layout/dialog_emojicompat.xml delete mode 100644 app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt diff --git a/app/build.gradle b/app/build.gradle index 02d7907d8..4484ff54e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 3f559399e..3e2030591 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -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 = 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 diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 0339a7bcc..ded947a84 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index 1a60d8603..9ffeca9ea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index ddecd20c7..7a7d5ecc4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt index dca696d84..2a1c74467 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt @@ -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, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt deleted file mode 100644 index 47cb37ae7..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ /dev/null @@ -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() - private var updated = false - private var currentNeedsUpdate = false - - private val downloadDisposables = MutableList(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" - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index 05438b722..74d8c8159 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -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() diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt index 1569cb151..852700811 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -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 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 diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt deleted file mode 100644 index 385be6c12..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt +++ /dev/null @@ -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>>? = 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>> { - // 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 { - if (version == null) return listOf(0) - return version.split(".").map { - it.toIntOrNull() ?: 0 - } - } - - fun downloadFontFile( - context: Context, - okHttpClient: OkHttpClient - ): Observable { - return Observable.create { emitter: ObservableEmitter -> - // 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, versionB: List): 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") - } - } - } -} diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 89a56d9e6..31b37ad89 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -112,7 +112,7 @@ app:layout_constraintStart_toStartOf="@id/guideAvatar" app:layout_constraintTop_toTopOf="@+id/accountFollowButton" /> - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml index a7b3a0ef6..c1565e093 100644 --- a/app/src/main/res/layout/item_account.xml +++ b/app/src/main/res/layout/item_account.xml @@ -32,7 +32,7 @@ tools:src="#000" tools:visibility="visible" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -