diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index e25a4564..78b4be88 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -55,6 +55,9 @@
public static *** v(...);
public static *** i(...);
}
+-assumenosideeffects class java.lang.String {
+ public static java.lang.String format(...);
+}
# remove some kotlin overhead
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
@@ -62,8 +65,3 @@
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
static void throwUninitializedPropertyAccessException(java.lang.String);
}
-
-# without this emoji font downloading fails with AbstractMethodError
--keep class * extends android.os.AsyncTask {
- public *;
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 53564ace..72aa387a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -117,7 +117,7 @@
android:name=".AccountActivity"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
-
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java b/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
deleted file mode 100644
index 8837c7d8..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
+++ /dev/null
@@ -1,291 +0,0 @@
-package com.keylesspalace.tusky;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-import android.widget.RadioButton;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.appcompat.app.AlertDialog;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceManager;
-
-import com.keylesspalace.tusky.util.EmojiCompatFont;
-
-import java.util.ArrayList;
-
-/**
- * This Preference lets the user select their preferred emoji font
- */
-public class EmojiPreference extends Preference {
- private static final String TAG = "EmojiPreference";
- private EmojiCompatFont selected, original;
- static final String FONT_PREFERENCE = "selected_emoji_font";
- private static final EmojiCompatFont[] FONTS = EmojiCompatFont.FONTS;
- // Please note that this array should be sorted in the same way as their fonts.
- private static final int[] viewIds = {
- R.id.item_nomoji,
- R.id.item_blobmoji,
- R.id.item_twemoji,
- R.id.item_notoemoji};
-
- private ArrayList radioButtons = new ArrayList<>();
-
- private boolean updated, currentNeedsUpdate;
-
- public EmojiPreference(Context context) {
- super(context);
-
- // Find out which font is currently active
- this.selected = EmojiCompatFont.byId(PreferenceManager
- .getDefaultSharedPreferences(context)
- .getInt(FONT_PREFERENCE, 0));
- // We'll use this later to determine if anything has changed
- this.original = this.selected;
-
- setSummary(selected.getDisplay(context));
- }
-
- public EmojiPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- // Find out which font is currently active
- this.selected = EmojiCompatFont.byId(PreferenceManager
- .getDefaultSharedPreferences(context)
- .getInt(FONT_PREFERENCE, 0));
- // We'll use this later to determine if anything has changed
- this.original = this.selected;
-
- setSummary(selected.getDisplay(context));
- }
-
- @Override
- protected void onClick() {
-
- View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_emojicompat, null);
-
- for (int i = 0; i < viewIds.length; i++) {
- setupItem(view.findViewById(viewIds[i]), FONTS[i]);
- }
-
- new AlertDialog.Builder(getContext())
- .setView(view)
- .setPositiveButton(android.R.string.ok, (dialog, which) -> onDialogOk())
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
-
- private void setupItem(View container, EmojiCompatFont font) {
- Context context = container.getContext();
-
- TextView title = container.findViewById(R.id.emojicompat_name);
- TextView caption = container.findViewById(R.id.emojicompat_caption);
- ImageView thumb = container.findViewById(R.id.emojicompat_thumb);
- ImageButton download = container.findViewById(R.id.emojicompat_download);
- ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
- RadioButton radio = container.findViewById(R.id.emojicompat_radio);
-
- // Initialize all the views
- title.setText(font.getDisplay(context));
- caption.setText(font.getCaption(context));
- thumb.setImageDrawable(font.getThumb(context));
-
- // There needs to be a list of all the radio buttons in order to uncheck them when one is selected
- radioButtons.add(radio);
-
- updateItem(font, container);
-
- // Set actions
- download.setOnClickListener((downloadButton) ->
- startDownload(font, container));
-
- cancel.setOnClickListener((cancelButton) ->
- cancelDownload(font, container));
-
- radio.setOnClickListener((radioButton) ->
- select(font, (RadioButton) radioButton));
-
- container.setOnClickListener((containterView) ->
- select(font,
- containterView.findViewById(R.id.emojicompat_radio
- )));
- }
-
- private void startDownload(EmojiCompatFont font, View container) {
- ImageButton download = container.findViewById(R.id.emojicompat_download);
- TextView caption = container.findViewById(R.id.emojicompat_caption);
-
- ProgressBar progressBar = container.findViewById(R.id.emojicompat_progress);
- ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
-
- // Switch to downloading style
- download.setVisibility(View.GONE);
- caption.setVisibility(View.INVISIBLE);
- progressBar.setVisibility(View.VISIBLE);
- cancel.setVisibility(View.VISIBLE);
-
-
- font.downloadFont(getContext(), new EmojiCompatFont.Downloader.EmojiDownloadListener() {
- @Override
- public void onDownloaded(EmojiCompatFont font) {
- finishDownload(font, container);
- }
-
- @Override
- public void onProgress(float progress) {
- // The progress is returned as a float between 0 and 1
- progress *= progressBar.getMax();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- progressBar.setProgress((int) progress, true);
- } else {
- progressBar.setProgress((int) progress);
- }
- }
-
- @Override
- public void onFailed() {
- Toast.makeText(getContext(), R.string.download_failed, Toast.LENGTH_SHORT).show();
- updateItem(font, container);
- }
- });
- }
-
- private void cancelDownload(EmojiCompatFont font, View container) {
- font.cancelDownload();
- updateItem(font, container);
- }
-
- private void finishDownload(EmojiCompatFont font, View container) {
- select(font, container.findViewById(R.id.emojicompat_radio));
- updateItem(font, container);
- // 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 void select(EmojiCompatFont font, RadioButton radio) {
- selected = font;
- // Uncheck all the other buttons
- for (RadioButton other : radioButtons) {
- if (other != radio) {
- other.setChecked(false);
- }
- }
- radio.setChecked(true);
- }
-
- /**
- * Called when a "consistent" state is reached, i.e. it's not downloading the font
- *
- * @param font The font to be displayed
- * @param container The ConstraintLayout containing the item
- */
- private void updateItem(EmojiCompatFont font, View container) {
- // Assignments
- ImageButton download = container.findViewById(R.id.emojicompat_download);
- TextView caption = container.findViewById(R.id.emojicompat_caption);
-
- ProgressBar progress = container.findViewById(R.id.emojicompat_progress);
- ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
-
- RadioButton radio = container.findViewById(R.id.emojicompat_radio);
-
- // There's no download going on
- progress.setVisibility(View.GONE);
- cancel.setVisibility(View.GONE);
- caption.setVisibility(View.VISIBLE);
-
- if (font.isDownloaded(getContext())) {
- // Make it selectable
- download.setVisibility(View.GONE);
- radio.setVisibility(View.VISIBLE);
- container.setClickable(true);
- } else {
- // Make it downloadable
- download.setVisibility(View.VISIBLE);
- radio.setVisibility(View.GONE);
- container.setClickable(false);
- }
-
- // Select it if necessary
- if (font == selected) {
- radio.setChecked(true);
- // Update available
- if (!font.isDownloaded(getContext())) {
- currentNeedsUpdate = true;
- }
- } else {
- radio.setChecked(false);
- }
- }
-
-
- /**
- * In order to be able to use this font later on, it needs to be saved first.
- */
- private void saveSelectedFont() {
- int index = selected.getId();
- Log.i(TAG, "saveSelectedFont: Font ID: " + index);
- // It's saved using the key FONT_PREFERENCE
- PreferenceManager
- .getDefaultSharedPreferences(getContext())
- .edit()
- .putInt(FONT_PREFERENCE, index)
- .apply();
- setSummary(selected.getDisplay(getContext()));
- }
-
- /**
- * That's it. The user doesn't want to switch between these amazing radio buttons anymore!
- * That means, the selected font can be saved (if the user hit OK)
- */
- private void onDialogOk() {
- saveSelectedFont();
- if (selected != original || updated) {
- new AlertDialog.Builder(getContext())
- .setTitle(R.string.restart_required)
- .setMessage(R.string.restart_emoji)
- .setNegativeButton(R.string.later, null)
- .setPositiveButton(R.string.restart, ((dialog, which) -> {
- // Restart the app
- // From https://stackoverflow.com/a/17166729/5070653
- Intent launchIntent = new Intent(getContext(), SplashActivity.class);
- PendingIntent mPendingIntent = PendingIntent.getActivity(
- getContext(),
- // This is the codepoint of the party face emoji :D
- 0x1f973,
- launchIntent,
- PendingIntent.FLAG_CANCEL_CURRENT);
- AlarmManager mgr =
- (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
- if (mgr != null) {
- mgr.set(
- AlarmManager.RTC,
- System.currentTimeMillis() + 100,
- mPendingIntent);
- }
- System.exit(0);
- })).show();
- }
-
- }
-
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
index 9ba9119d..59b4e937 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
@@ -52,6 +52,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
import com.keylesspalace.tusky.components.notifications.NotificationHelper
+import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.db.AccountEntity
diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
index f41395bd..7fb762d2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
@@ -24,6 +24,7 @@ import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector
+import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.uber.autodispose.AutoDisposePlugins
import dagger.android.DispatchingAndroidInjector
@@ -53,7 +54,7 @@ class TuskyApplication : Application(), HasAndroidInjector {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
// init the custom emoji fonts
- val emojiSelection = preferences.getInt(EmojiPreference.FONT_PREFERENCE, 0)
+ val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0)
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
.getConfig(this)
.setReplaceAll(true)
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
similarity index 99%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
index 24049aa9..af882977 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.content.Intent
import android.graphics.drawable.Drawable
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
new file mode 100644
index 00000000..c0e538a9
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt
@@ -0,0 +1,258 @@
+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.*
+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.util.EmojiCompatFont
+import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.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 view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null)
+ viewIds.forEachIndexed { index, viewId ->
+ setupItem(view.findViewById(viewId), FONTS[index])
+ }
+ AlertDialog.Builder(context)
+ .setView(view)
+ .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ private fun setupItem(container: View, font: EmojiCompatFont) {
+ val title: TextView = container.findViewById(R.id.emojicompat_name)
+ val caption: TextView = container.findViewById(R.id.emojicompat_caption)
+ val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb)
+ val download: ImageButton = container.findViewById(R.id.emojicompat_download)
+ val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
+ val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
+
+ // Initialize all the views
+ title.text = font.getDisplay(container.context)
+ caption.setText(font.caption)
+ thumb.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(radio)
+ updateItem(font, container)
+
+ // Set actions
+ download.setOnClickListener { startDownload(font, container) }
+ cancel.setOnClickListener { cancelDownload(font, container) }
+ radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
+ container.setOnClickListener { containerView: View ->
+ select(font, containerView.findViewById(R.id.emojicompat_radio))
+ }
+ }
+
+ private fun startDownload(font: EmojiCompatFont, container: View) {
+ val download: ImageButton = container.findViewById(R.id.emojicompat_download)
+ val caption: TextView = container.findViewById(R.id.emojicompat_caption)
+ val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress)
+ val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
+
+ // Switch to downloading style
+ download.visibility = View.GONE
+ caption.visibility = View.INVISIBLE
+ progressBar.visibility = View.VISIBLE
+ progressBar.progress = 0
+ cancel.visibility = View.VISIBLE
+ 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) {
+ progressBar.isIndeterminate = false
+ val max = progressBar.max.toFloat()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ progressBar.setProgress((max * progress).toInt(), true)
+ } else {
+ progressBar.progress = (max * progress).toInt()
+ }
+ } else {
+ progressBar.isIndeterminate = true
+ }
+ },
+ {
+ Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
+ updateItem(font, container)
+ },
+ {
+ finishDownload(font, container)
+ }
+ ).also { downloadDisposables[font.id] = it }
+
+
+ }
+
+ private fun cancelDownload(font: EmojiCompatFont, container: View) {
+ font.deleteDownloadedFile(container.context)
+ downloadDisposables[font.id]?.dispose()
+ downloadDisposables[font.id] = null
+ updateItem(font, container)
+ }
+
+ private fun finishDownload(font: EmojiCompatFont, container: View) {
+ select(font, container.findViewById(R.id.emojicompat_radio))
+ updateItem(font, container)
+ // 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
+ // Uncheck all the other buttons
+ for (other in radioButtons) {
+ if (other !== radio) {
+ other.isChecked = false
+ }
+ }
+ radio.isChecked = true
+ }
+
+ /**
+ * Called when a "consistent" state is reached, i.e. it's not downloading the font
+ *
+ * @param font The font to be displayed
+ * @param container The ConstraintLayout containing the item
+ */
+ private fun updateItem(font: EmojiCompatFont, container: View) {
+ // Assignments
+ val download: ImageButton = container.findViewById(R.id.emojicompat_download)
+ val caption: TextView = container.findViewById(R.id.emojicompat_caption)
+ val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress)
+ val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
+ val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
+
+ // There's no download going on
+ progress.visibility = View.GONE
+ cancel.visibility = View.GONE
+ caption.visibility = View.VISIBLE
+ if (font.isDownloaded(context)) {
+ // Make it selectable
+ download.visibility = View.GONE
+ radio.visibility = View.VISIBLE
+ container.isClickable = true
+ } else {
+ // Make it downloadable
+ download.visibility = View.VISIBLE
+ radio.visibility = View.GONE
+ container.isClickable = false
+ }
+
+ // Select it if necessary
+ if (font === selected) {
+ radio.isChecked = true
+ // Update available
+ if (!font.isDownloaded(context)) {
+ currentNeedsUpdate = true
+ }
+ } else {
+ radio.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,
+ PendingIntent.FLAG_CANCEL_CURRENT)
+ 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"
+
+ // Please note that this array must sorted in the same way as the fonts.
+ private val viewIds = intArrayOf(
+ R.id.item_nomoji,
+ R.id.item_blobmoji,
+ R.id.item_twemoji,
+ R.id.item_notoemoji
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
similarity index 99%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
index 800ea7c1..f917d2bb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
similarity index 97%
rename from app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
index 383a07ae..4fe0abd8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky
+package com.keylesspalace.tusky.components.preference
import android.content.Context
import android.content.Intent
@@ -23,9 +23,11 @@ import android.util.Log
import android.view.MenuItem
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
+import com.keylesspalace.tusky.BaseActivity
+import com.keylesspalace.tusky.MainActivity
+import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
-import com.keylesspalace.tusky.fragment.preference.*
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
similarity index 96%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
index 6dec7273..edddffca 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
@@ -13,13 +13,13 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
-import com.keylesspalace.tusky.PreferencesActivity
import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
@@ -27,8 +27,13 @@ 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 javax.inject.Inject
-class PreferencesFragment : PreferenceFragmentCompat() {
+class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
+
+ @Inject
+ lateinit var okhttpclient: OkHttpClient
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null
@@ -47,7 +52,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
}
- emojiPreference {
+ emojiPreference(okhttpclient) {
setDefaultValue("system_default")
setIcon(R.drawable.ic_emoji_24dp)
key = PrefKeys.EMOJI
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt
similarity index 97%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt
index e7ee7ade..922d5a7a 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt
similarity index 97%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt
index cd76300d..71c5e10e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
index 1f958e5a..ea2741ca 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
+import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
index 48e8b625..a1a8c8fe 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
@@ -20,14 +20,15 @@ import com.keylesspalace.tusky.AccountsInListFragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
import com.keylesspalace.tusky.fragment.*
-import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
-import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
+import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment
+import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment
+import com.keylesspalace.tusky.components.preference.PreferencesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@@ -85,4 +86,7 @@ abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract fun searchHashtagsFragment(): SearchHashtagsFragment
+ @ContributesAndroidInjector
+ abstract fun preferencesFragment(): PreferencesFragment
+
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
index 0a5d3494..df9627d8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
@@ -17,7 +17,7 @@ object PrefKeys {
// each preference a key for it to work.
const val APP_THEME = "appTheme"
- const val EMOJI = "emojiCompat"
+ const val EMOJI = "selected_emoji_font"
const val FAB_HIDE = "fabHide"
const val LANGUAGE = "language"
const val STATUS_TEXT_SIZE = "statusTextSize"
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 438580ee..82dfa14e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt
@@ -3,7 +3,8 @@ package com.keylesspalace.tusky.settings
import android.content.Context
import androidx.annotation.StringRes
import androidx.preference.*
-import com.keylesspalace.tusky.EmojiPreference
+import com.keylesspalace.tusky.components.preference.EmojiPreference
+import okhttp3.OkHttpClient
class PreferenceParent(
val context: Context,
@@ -24,8 +25,8 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit):
return pref
}
-inline fun PreferenceParent.emojiPreference(builder: EmojiPreference.() -> Unit): EmojiPreference {
- val pref = EmojiPreference(context)
+inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference {
+ val pref = EmojiPreference(context, okHttpClient)
builder(pref)
addPref(pref)
return pref
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java
deleted file mode 100644
index ca7899e7..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java
+++ /dev/null
@@ -1,564 +0,0 @@
-package com.keylesspalace.tusky.util;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
-
-import com.keylesspalace.tusky.R;
-
-import java.io.EOFException;
-import java.io.File;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import de.c1710.filemojicompat.FileEmojiCompatConfig;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
-import okio.BufferedSink;
-import okio.Okio;
-import okio.Source;
-
-
-/**
- * This class bundles information about an emoji font as well as many convenient actions.
- */
-public class EmojiCompatFont {
- private static final String TAG = "EmojiCompatFont";
- /**
- * This String represents the sub-directory the fonts are stored in.
- */
- private static final String DIRECTORY = "emoji";
-
- // These are the items which are also present in the JSON files
- private final String name, display, url;
- // The thumbnail image and the caption are provided as resource ids
- private final int img, caption;
- // The version is stored as a String in the x.xx.xx format (to be able to compare versions)
- private final String version;
- private final int[] versionCode;
- private AsyncTask fontDownloader;
- // The system font gets some special behavior...
- private static final EmojiCompatFont SYSTEM_DEFAULT =
- new EmojiCompatFont("system-default",
- "System Default",
- R.string.caption_systememoji,
- R.drawable.ic_emoji_34dp,
- "",
- "0");
- private static final EmojiCompatFont BLOBMOJI =
- new EmojiCompatFont("Blobmoji",
- "Blobmoji",
- R.string.caption_blobmoji,
- R.drawable.ic_blobmoji,
- "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
- "12.0.0"
- );
- private static final EmojiCompatFont TWEMOJI =
- new EmojiCompatFont("Twemoji",
- "Twemoji",
- R.string.caption_twemoji,
- R.drawable.ic_twemoji,
- "https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
- "12.0.0"
- );
- private static final EmojiCompatFont NOTOEMOJI =
- new EmojiCompatFont("NotoEmoji",
- "Noto Emoji",
- R.string.caption_notoemoji,
- R.drawable.ic_notoemoji,
- "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
- "11.0.0"
- );
-
- /**
- * This array stores all available EmojiCompat fonts.
- * References to them can simply be saved by saving their indices
- */
- public static final EmojiCompatFont[] FONTS = {SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI};
- // A list of all available font files and whether they are older than the current version or not
- // They are ordered by there version codes in ascending order
- private ArrayList> existingFontFiles;
-
- private EmojiCompatFont(String name,
- String display,
- int caption,
- int img,
- String url,
- String version) {
- this.name = name;
- this.display = display;
- this.caption = caption;
- this.img = img;
- this.url = url;
- this.version = version;
- this.versionCode = getVersionCode(version);
- }
-
- /**
- * 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.
- */
- public static EmojiCompatFont byId(int id) {
- if (id >= 0 && id < FONTS.length) {
- return FONTS[id];
- } else {
- return SYSTEM_DEFAULT;
- }
- }
-
- public int getId() {
- return Arrays.asList(FONTS).indexOf(this);
- }
-
- public String getName() {
- return name;
- }
-
-
- public String getDisplay(Context context) {
- return this != SYSTEM_DEFAULT ? display : context.getString(R.string.system_default);
- }
-
- public String getCaption(Context context) {
- return context.getResources().getString(caption);
- }
-
- public String getUrl() {
- return url;
- }
-
- public Drawable getThumb(Context context) {
- return ContextCompat.getDrawable(context, img);
- }
-
- public String getVersion() {
- return version;
- }
-
- public int[] getVersionCode() {
- return versionCode;
- }
-
- /**
- * 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
- */
- @Nullable
- private File getFont(Context context) {
- if (this != SYSTEM_DEFAULT) {
- File directory = new File(context.getExternalFilesDir(null), DIRECTORY);
- return new File(directory, this.getName() + this.getVersion() + ".ttf");
- } else {
- return null;
- }
- }
-
-
- public FileEmojiCompatConfig getConfig(Context context) {
- return new FileEmojiCompatConfig(context, getLatestFontFile(context));
- }
-
- public boolean isDownloaded(Context context) {
- // The existence of the current version is actually checked twice, although the first method should
- // be much faster and more common.
- return this == SYSTEM_DEFAULT || getFont(context) != null
- && (getFont(context).exists() || newerFileExists(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 boolean newerFileExists(Context context) {
- loadExistingFontFiles(context);
- if (!existingFontFiles.isEmpty())
- // The last file is already the newest one...
- return compareVersions(existingFontFiles.get(existingFontFiles.size() - 1).second,
- getVersionCode()) >= 0;
- return false;
- }
-
- /**
- * Downloads the TTF file for this font
- *
- * @param listeners The listeners which will be notified when the download has been finished
- */
- public void downloadFont(Context context, Downloader.EmojiDownloadListener... listeners) {
- if (this != SYSTEM_DEFAULT) {
- // Additionally run a cleanup process after the download has been successful.
- Downloader.EmojiDownloadListener cleanup = font -> deleteOldVersions(context);
-
- List allListeners
- = new ArrayList<>(Arrays.asList(listeners));
- allListeners.add(cleanup);
- Downloader.EmojiDownloadListener[] allListenersA =
- new Downloader.EmojiDownloadListener[allListeners.size()];
-
- fontDownloader = new Downloader(
- this,
- allListeners.toArray(allListenersA))
- .execute(getFont(context));
- } else {
- for (Downloader.EmojiDownloadListener listener : listeners) {
- // The system emoji font is always downloaded...
- listener.onDownloaded(this);
- }
- }
- }
-
- /**
- * Deletes any older version of a font
- *
- * @param context The current Context
- */
- private void deleteOldVersions(Context context) {
- loadExistingFontFiles(context);
- Log.d(TAG, "deleting old versions...");
-
- Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size()));
- for (Pair fileExists : existingFontFiles) {
- if (compareVersions(fileExists.second, getVersionCode()) < 0) {
- File file = fileExists.first;
- // Uses side effects!
- Log.d(TAG, String.format("Deleted %s successfully: %s", file.getAbsolutePath(),
- file.delete()));
- }
- }
- }
-
- private static final Comparator> pairComparator = (o1, o2) -> compareVersions(o1.second, o2.second);
-
-
- /**
- * 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 void loadExistingFontFiles(Context context) {
- // Only load it once
- if (this.existingFontFiles == null) {
- // If we call this on the system default font, just return nothing...
- if (this == SYSTEM_DEFAULT) {
- existingFontFiles = new ArrayList<>(0);
- }
-
- File directory = new 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.
- Pattern fontRegex = Pattern.compile(getName() + "(\\d+(\\.\\d+)*)?" + "\\.ttf");
-
-
- FilenameFilter ttfFilter = (dir, name) -> name.endsWith(".ttf");
- File[] existingFontFiles = directory.isDirectory() ? directory.listFiles(ttfFilter) : new File[0];
- Log.d(TAG, String.format("loadExistingFontFiles: %d other font files found",
- existingFontFiles.length));
- // This is actually the upper bound
- this.existingFontFiles = new ArrayList<>(existingFontFiles.length);
-
-
- for (File file : existingFontFiles) {
- Matcher matcher = fontRegex.matcher(file.getName());
- if (matcher.matches()) {
- String version = matcher.group(1);
- int[] versionCode = getVersionCode(version);
- Pair entry = new Pair<>(file, versionCode);
- // https://stackoverflow.com/a/51893026
- // Insert it in a sorted way
- int index = Collections.binarySearch(this.existingFontFiles, entry, pairComparator);
- if (index < 0) {
- index = -index - 1;
- }
- this.existingFontFiles.add(index, entry);
- }
- }
- }
- }
-
- /**
- * 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 File getLatestFontFile(@NonNull Context context) {
- File current = getFont(context);
- if (current != null && current.exists())
- return current;
- loadExistingFontFiles(context);
- try {
- return existingFontFiles.get(existingFontFiles.size() - 1).first;
- } catch (IndexOutOfBoundsException e) {
- return getFont(context);
- }
- }
-
- private @Nullable
- int[] getVersionCode(@Nullable String version) {
- if (version == null)
- return null;
- String[] versions = version.split("\\.");
- int[] versionCodes = new int[versions.length];
- for (int i = 0; i < versions.length; i++)
- versionCodes[i] = parseInt(versions[i], 0);
- return versionCodes;
- }
-
- /**
- * A small helper method to convert a String to an int with a default value
- *
- * @param value The String to be parsed
- * @param def The default value
- * @return Either the String parsed to an int or - if this is not possible - the default value
- */
- private int parseInt(@Nullable String value, int def) {
- try {
- return Integer.parseInt(value);
- } catch (NumberFormatException | NullPointerException e) {
- e.printStackTrace();
- return def;
- }
- }
-
- /**
- * 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
- */
- private static int compareVersions(int[] versionA, int[] versionB) {
- // This saves us much headache about handling a null version
- if (versionA == null)
- versionA = new int[]{0};
-
- int len = Math.max(versionB.length, versionA.length);
-
- int vA, vB;
- // Compare the versions
- for (int i = 0; i < len; i++) {
- // Just to make sure there is something specified here
- if (versionA.length > i) {
- vA = versionA[i];
- } else {
- vA = 0;
- }
- if (versionB.length > i) {
- vB = versionB[i];
- } else {
- vB = 0;
- }
-
- // It needs to be decided on the next level
- if (vB == vA)
- continue;
- // Okay, is version B newer or version A?
- return Integer.compare(vA, vB);
- }
-
- // The versions are equal
- return 0;
- }
-
- /**
- * Stops downloading the font. If no one started a font download, nothing happens.
- */
- public void cancelDownload() {
- if (fontDownloader != null) {
- fontDownloader.cancel(false);
- fontDownloader = null;
- }
- }
-
- /**
- * This class is used to easily manage the download of a font
- */
- public static class Downloader extends AsyncTask {
- // All interested objects/methods
- private final EmojiDownloadListener[] listeners;
- // The MIME-Type which might be unnecessary
- private static final String MIME = "application/woff";
- // The font belonging to this download
- private final EmojiCompatFont font;
- private static final String TAG = "Emoji-Font Downloader";
- private static long CHUNK_SIZE = 4096;
- private boolean failed = false;
-
- Downloader(EmojiCompatFont font, EmojiDownloadListener... listeners) {
- super();
- this.listeners = listeners;
- this.font = font;
- }
-
- @Override
- protected File doInBackground(File... files) {
- // Only download to one file...
- File downloadFile = files[0];
- try {
- // It is possible (and very likely) that the file does not exist yet
- if (!downloadFile.exists()) {
- downloadFile.getParentFile().mkdirs();
- downloadFile.createNewFile();
- }
- OkHttpClient client = new OkHttpClient();
- Request request = new Request.Builder().url(font.getUrl())
- .addHeader("Content-Type", MIME)
- .build();
- Response response = client.newCall(request).execute();
- BufferedSink sink = Okio.buffer(Okio.sink(downloadFile));
- Source source = null;
- try {
- long size;
- // Download!
- if (response.body() != null
- && response.isSuccessful()
- && (size = networkResponseLength(response)) > 0) {
- float progress = 0;
- source = response.body().source();
- try {
- while (!isCancelled()) {
- sink.write(response.body().source(), CHUNK_SIZE);
- progress += CHUNK_SIZE;
- publishProgress(progress / size);
- }
- } catch (EOFException ex) {
- /*
- 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 " + font.getUrl() + " failed. No content to download.");
- Log.e(TAG, "Status code: " + response.code());
- failed = true;
- }
- } finally {
- if (source != null) {
- source.close();
- }
- sink.close();
- // This 'if' uses side effects to delete the File.
- if (isCancelled() && !downloadFile.delete()) {
- Log.e(TAG, "Could not delete file " + downloadFile);
- }
- }
- } catch (IOException ex) {
- ex.printStackTrace();
- failed = true;
- }
- return downloadFile;
- }
-
- @Override
- public void onProgressUpdate(Float... progress) {
- for (EmojiDownloadListener listener : listeners) {
- listener.onProgress(progress[0]);
- }
- }
-
- @Override
- public void onPostExecute(File downloadedFile) {
- if (!failed && downloadedFile.exists()) {
- for (EmojiDownloadListener listener : listeners) {
- listener.onDownloaded(font);
- }
- } else {
- fail(downloadedFile);
- }
- }
-
- private void fail(File failedFile) {
- if (failedFile.exists() && !failedFile.delete()) {
- Log.e(TAG, "Could not delete file " + failedFile);
- }
- for (EmojiDownloadListener listener : listeners) {
- listener.onFailed();
- }
- }
-
- /**
- * This interfaced is used to get notified when a download has been finished
- */
- public interface EmojiDownloadListener {
- /**
- * Called after successfully finishing a download.
- *
- * @param font The font related to this download. This will help identifying the download
- */
- void onDownloaded(EmojiCompatFont font);
-
- // TODO: Add functionality
-
- /**
- * Called when something went wrong with the download.
- * This one won't be called when the download has been cancelled though.
- */
- default void onFailed() {
- // Oh no! D:
- }
-
- /**
- * Called whenever the progress changed
- *
- * @param Progress A value between 0 and 1 representing the current progress
- */
- default void onProgress(float Progress) {
- // ARE WE THERE YET?
- }
- }
-
-
- /**
- * This method is needed because when transparent compression is used OkHttp reports
- * {@link ResponseBody#contentLength()} as -1. We try to get the header which server sent
- * us manually here.
- *
- * @see OkHttp issue 259
- */
- private long networkResponseLength(Response response) {
- Response networkResponse = response.networkResponse();
- if (networkResponse == null) {
- // In case it's a fully cached response
- ResponseBody body = response.body();
- return body == null ? -1 : body.contentLength();
- }
- String header = networkResponse.header("Content-Length");
- if (header == null) {
- return -1;
- }
- try {
- return Integer.parseInt(header);
- } catch (NumberFormatException e) {
- return -1;
- }
- }
- }
-
- @Override
- @NonNull
- public String toString() {
- return display;
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt
new file mode 100644
index 00000000..e18e24ad
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt
@@ -0,0 +1,351 @@
+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.Observable
+import io.reactivex.ObservableEmitter
+import io.reactivex.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(
+ Comparator>> { 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...
+ private val SYSTEM_DEFAULT = EmojiCompatFont("system-default",
+ "System Default",
+ R.string.caption_systememoji,
+ R.drawable.ic_emoji_34dp,
+ "",
+ "0")
+ private val BLOBMOJI = EmojiCompatFont("Blobmoji",
+ "Blobmoji",
+ R.string.caption_blobmoji,
+ R.drawable.ic_blobmoji,
+ "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
+ "12.0.0"
+ )
+ private val TWEMOJI = EmojiCompatFont("Twemoji",
+ "Twemoji",
+ R.string.caption_twemoji,
+ R.drawable.ic_twemoji,
+ "https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
+ "12.0.0"
+ )
+ private val NOTOEMOJI = EmojiCompatFont("NotoEmoji",
+ "Noto Emoji",
+ R.string.caption_notoemoji,
+ R.drawable.ic_notoemoji,
+ "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
+ "11.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")
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_preferences.xml b/app/src/main/res/layout/activity_preferences.xml
index 3fa27457..1931fe08 100644
--- a/app/src/main/res/layout/activity_preferences.xml
+++ b/app/src/main/res/layout/activity_preferences.xml
@@ -5,7 +5,7 @@
android:id="@+id/activity_view_thread"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context="com.keylesspalace.tusky.PreferencesActivity">
+ tools:context="com.keylesspalace.tusky.components.preference.PreferencesActivity">
diff --git a/app/src/main/res/layout/dialog_emojicompat.xml b/app/src/main/res/layout/dialog_emojicompat.xml
index 923b69ea..6850e045 100644
--- a/app/src/main/res/layout/dialog_emojicompat.xml
+++ b/app/src/main/res/layout/dialog_emojicompat.xml
@@ -1,33 +1,25 @@
-
-
+
-
+
-
+
-
-
-
-
+
+ android:textColor="?android:attr/textColorSecondary" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt
new file mode 100644
index 00000000..badaa709
--- /dev/null
+++ b/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt
@@ -0,0 +1,47 @@
+package com.keylesspalace.tusky.util
+
+import org.junit.Assert.*
+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)
+ )
+ )
+ }
+}
\ No newline at end of file