convert EmojiPreference and EmojiCompatFont to Kotlin (#1922)
* convert EmojiPreference and EmojiCompatFont to Kotlin * move preference related to to dedicated preference package * update proguard-rules.pro * reformat & add comment * maintain disposable information in EmojiPreference instead of EmojiCompatFont
This commit is contained in:
parent
fc7b02d987
commit
1d309850b0
|
@ -55,6 +55,9 @@
|
||||||
public static *** v(...);
|
public static *** v(...);
|
||||||
public static *** i(...);
|
public static *** i(...);
|
||||||
}
|
}
|
||||||
|
-assumenosideeffects class java.lang.String {
|
||||||
|
public static java.lang.String format(...);
|
||||||
|
}
|
||||||
|
|
||||||
# remove some kotlin overhead
|
# remove some kotlin overhead
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
|
@ -62,8 +65,3 @@
|
||||||
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
||||||
static void throwUninitializedPropertyAccessException(java.lang.String);
|
static void throwUninitializedPropertyAccessException(java.lang.String);
|
||||||
}
|
}
|
||||||
|
|
||||||
# without this emoji font downloading fails with AbstractMethodError
|
|
||||||
-keep class * extends android.os.AsyncTask {
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
android:name=".AccountActivity"
|
android:name=".AccountActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
||||||
<activity android:name=".EditProfileActivity" />
|
<activity android:name=".EditProfileActivity" />
|
||||||
<activity android:name=".PreferencesActivity" />
|
<activity android:name=".components.preference.PreferencesActivity" />
|
||||||
<activity android:name=".StatusListActivity" />
|
<activity android:name=".StatusListActivity" />
|
||||||
<activity android:name=".AccountListActivity" />
|
<activity android:name=".AccountListActivity" />
|
||||||
<activity android:name=".AboutActivity" />
|
<activity android:name=".AboutActivity" />
|
||||||
|
|
|
@ -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<RadioButton> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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.compose.ComposeActivity.Companion.canHandleMimeType
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
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.scheduled.ScheduledTootActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
|
|
@ -24,6 +24,7 @@ import androidx.preference.PreferenceManager
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
||||||
import com.keylesspalace.tusky.di.AppInjector
|
import com.keylesspalace.tusky.di.AppInjector
|
||||||
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.*
|
||||||
import com.uber.autodispose.AutoDisposePlugins
|
import com.uber.autodispose.AutoDisposePlugins
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
|
@ -53,7 +54,7 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
// init the custom emoji fonts
|
// 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)
|
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
|
||||||
.getConfig(this)
|
.getConfig(this)
|
||||||
.setReplaceAll(true)
|
.setReplaceAll(true)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment.preference
|
package com.keylesspalace.tusky.components.preference
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
|
@ -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<RadioButton>()
|
||||||
|
private var updated = false
|
||||||
|
private var currentNeedsUpdate = false
|
||||||
|
|
||||||
|
private val downloadDisposables = MutableList<Disposable?>(FONTS.size) { null }
|
||||||
|
|
||||||
|
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
|
||||||
|
super.onAttachedToHierarchy(preferenceManager)
|
||||||
|
|
||||||
|
// Find out which font is currently active
|
||||||
|
selected = EmojiCompatFont.byId(
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
|
||||||
|
)
|
||||||
|
// We'll use this later to determine if anything has changed
|
||||||
|
original = selected
|
||||||
|
summary = selected.getDisplay(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
val 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment.preference
|
package com.keylesspalace.tusky.components.preference
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky.components.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -23,9 +23,11 @@ import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.preference.PreferenceManager
|
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.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.fragment.preference.*
|
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
|
@ -13,13 +13,13 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment.preference
|
package com.keylesspalace.tusky.components.preference
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.keylesspalace.tusky.PreferencesActivity
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.settings.*
|
import com.keylesspalace.tusky.settings.*
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
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.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizePx
|
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 val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
|
||||||
private var httpProxyPref: Preference? = null
|
private var httpProxyPref: Preference? = null
|
||||||
|
@ -47,7 +52,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
|
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiPreference {
|
emojiPreference(okhttpclient) {
|
||||||
setDefaultValue("system_default")
|
setDefaultValue("system_default")
|
||||||
setIcon(R.drawable.ic_emoji_24dp)
|
setIcon(R.drawable.ic_emoji_24dp)
|
||||||
key = PrefKeys.EMOJI
|
key = PrefKeys.EMOJI
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment.preference
|
package com.keylesspalace.tusky.components.preference
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment.preference
|
package com.keylesspalace.tusky.components.preference
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.*
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
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.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
|
|
|
@ -20,14 +20,15 @@ import com.keylesspalace.tusky.AccountsInListFragment
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||||
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
|
||||||
import com.keylesspalace.tusky.fragment.*
|
import com.keylesspalace.tusky.fragment.*
|
||||||
import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
|
import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment
|
||||||
import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
|
import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment
|
||||||
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
|
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
|
||||||
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
|
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
|
||||||
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
|
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
|
||||||
import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment
|
import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment
|
||||||
import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment
|
import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment
|
||||||
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment
|
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment
|
||||||
|
import com.keylesspalace.tusky.components.preference.PreferencesFragment
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
@ -85,4 +86,7 @@ abstract class FragmentBuildersModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun searchHashtagsFragment(): SearchHashtagsFragment
|
abstract fun searchHashtagsFragment(): SearchHashtagsFragment
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun preferencesFragment(): PreferencesFragment
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ object PrefKeys {
|
||||||
// each preference a key for it to work.
|
// each preference a key for it to work.
|
||||||
|
|
||||||
const val APP_THEME = "appTheme"
|
const val APP_THEME = "appTheme"
|
||||||
const val EMOJI = "emojiCompat"
|
const val EMOJI = "selected_emoji_font"
|
||||||
const val FAB_HIDE = "fabHide"
|
const val FAB_HIDE = "fabHide"
|
||||||
const val LANGUAGE = "language"
|
const val LANGUAGE = "language"
|
||||||
const val STATUS_TEXT_SIZE = "statusTextSize"
|
const val STATUS_TEXT_SIZE = "statusTextSize"
|
||||||
|
|
|
@ -3,7 +3,8 @@ package com.keylesspalace.tusky.settings
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import com.keylesspalace.tusky.EmojiPreference
|
import com.keylesspalace.tusky.components.preference.EmojiPreference
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class PreferenceParent(
|
class PreferenceParent(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
@ -24,8 +25,8 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit):
|
||||||
return pref
|
return pref
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun PreferenceParent.emojiPreference(builder: EmojiPreference.() -> Unit): EmojiPreference {
|
inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference {
|
||||||
val pref = EmojiPreference(context)
|
val pref = EmojiPreference(context, okHttpClient)
|
||||||
builder(pref)
|
builder(pref)
|
||||||
addPref(pref)
|
addPref(pref)
|
||||||
return pref
|
return pref
|
||||||
|
|
|
@ -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<Pair<File, int[]>> 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<Downloader.EmojiDownloadListener> 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<File, int[]> 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<Pair<File, int[]>> 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<File, int[]> 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<File, Float, File> {
|
|
||||||
// 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 <a href="https://github.com/square/okhttp/issues/259">OkHttp issue 259</a>
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Pair<File, List<Int>>>? = null
|
||||||
|
|
||||||
|
val id: Int
|
||||||
|
get() = FONTS.indexOf(this)
|
||||||
|
|
||||||
|
fun getDisplay(context: Context): String {
|
||||||
|
return if (this !== SYSTEM_DEFAULT) display else context.getString(R.string.system_default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will return the actual font file (regardless of its existence) for
|
||||||
|
* the current version (not necessarily the latest!).
|
||||||
|
*
|
||||||
|
* @return The font (TTF) file or null if called on SYSTEM_FONT
|
||||||
|
*/
|
||||||
|
private fun getFontFile(context: Context): File? {
|
||||||
|
return if (this !== SYSTEM_DEFAULT) {
|
||||||
|
val directory = File(context.getExternalFilesDir(null), DIRECTORY)
|
||||||
|
File(directory, "$name$version.ttf")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConfig(context: Context): FileEmojiCompatConfig {
|
||||||
|
return FileEmojiCompatConfig(context, getLatestFontFile(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDownloaded(context: Context): Boolean {
|
||||||
|
return this === SYSTEM_DEFAULT || getFontFile(context)?.exists() == true || fontFileExists(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether there is already a font version that satisfies the current version, i.e. it
|
||||||
|
* has a higher or equal version code.
|
||||||
|
*
|
||||||
|
* @param context The Context
|
||||||
|
* @return Whether there is a font file with a higher or equal version code to the current
|
||||||
|
*/
|
||||||
|
private fun fontFileExists(context: Context): Boolean {
|
||||||
|
val existingFontFiles = getExistingFontFiles(context)
|
||||||
|
return if (existingFontFiles.isNotEmpty()) {
|
||||||
|
compareVersions(existingFontFiles.last().second, versionCode) >= 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes any older version of a font
|
||||||
|
*
|
||||||
|
* @param context The current Context
|
||||||
|
*/
|
||||||
|
private fun deleteOldVersions(context: Context) {
|
||||||
|
val existingFontFiles = getExistingFontFiles(context)
|
||||||
|
Log.d(TAG, "deleting old versions...")
|
||||||
|
Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size))
|
||||||
|
for (fileExists in existingFontFiles) {
|
||||||
|
if (compareVersions(fileExists.second, versionCode) < 0) {
|
||||||
|
val file = fileExists.first
|
||||||
|
// Uses side effects!
|
||||||
|
Log.d(TAG, String.format("Deleted %s successfully: %s", file.absolutePath,
|
||||||
|
file.delete()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all font files that are inside the files directory into an ArrayList with the information
|
||||||
|
* on whether they are older than the currently available version or not.
|
||||||
|
*
|
||||||
|
* @param context The Context
|
||||||
|
*/
|
||||||
|
private fun getExistingFontFiles(context: Context): List<Pair<File, List<Int>>> {
|
||||||
|
// Only load it once
|
||||||
|
existingFontFileCache?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
// If we call this on the system default font, just return nothing...
|
||||||
|
if (this === SYSTEM_DEFAULT) {
|
||||||
|
existingFontFileCache = emptyList()
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val directory = File(context.getExternalFilesDir(null), DIRECTORY)
|
||||||
|
// It will search for old versions using a regex that matches the font's name plus
|
||||||
|
// (if present) a version code. No version code will be regarded as version 0.
|
||||||
|
val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern()
|
||||||
|
val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") }
|
||||||
|
val foundFontFiles = directory.listFiles(ttfFilter).orEmpty()
|
||||||
|
Log.d(TAG, String.format("loadExistingFontFiles: %d other font files found",
|
||||||
|
foundFontFiles.size))
|
||||||
|
|
||||||
|
return foundFontFiles.map { file ->
|
||||||
|
val matcher = fontRegex.matcher(file.name)
|
||||||
|
val versionCode = if (matcher.matches()) {
|
||||||
|
val version = matcher.group(1)
|
||||||
|
getVersionCode(version)
|
||||||
|
} else {
|
||||||
|
listOf(0)
|
||||||
|
}
|
||||||
|
Pair(file, versionCode)
|
||||||
|
}.sortedWith(
|
||||||
|
Comparator<Pair<File, List<Int>>> { a, b -> compareVersions(a.second, b.second) }
|
||||||
|
).also {
|
||||||
|
existingFontFileCache = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current or latest version of this font file (if there is any)
|
||||||
|
*
|
||||||
|
* @param context The Context
|
||||||
|
* @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font.
|
||||||
|
*/
|
||||||
|
private fun getLatestFontFile(context: Context): File? {
|
||||||
|
val current = getFontFile(context)
|
||||||
|
if (current != null && current.exists()) return current
|
||||||
|
val existingFontFiles = getExistingFontFiles(context)
|
||||||
|
return existingFontFiles.firstOrNull()?.first
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVersionCode(version: String?): List<Int> {
|
||||||
|
if (version == null) return listOf(0)
|
||||||
|
return version.split(".").map {
|
||||||
|
it.toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadFontFile(context: Context,
|
||||||
|
okHttpClient: OkHttpClient): Observable<Float> {
|
||||||
|
return Observable.create { emitter: ObservableEmitter<Float> ->
|
||||||
|
// It is possible (and very likely) that the file does not exist yet
|
||||||
|
val downloadFile = getFontFile(context)!!
|
||||||
|
if (!downloadFile.exists()) {
|
||||||
|
downloadFile.parentFile?.mkdirs()
|
||||||
|
downloadFile.createNewFile()
|
||||||
|
}
|
||||||
|
val request = Request.Builder().url(url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val sink = downloadFile.sink().buffer()
|
||||||
|
var source: Source? = null
|
||||||
|
try {
|
||||||
|
// Download!
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
|
val responseBody = response.body
|
||||||
|
if (response.isSuccessful && responseBody != null) {
|
||||||
|
val size = response.length()
|
||||||
|
var progress = 0f
|
||||||
|
source = responseBody.source()
|
||||||
|
try {
|
||||||
|
while (!emitter.isDisposed) {
|
||||||
|
sink.write(source, CHUNK_SIZE)
|
||||||
|
progress += CHUNK_SIZE.toFloat()
|
||||||
|
if(size > 0) {
|
||||||
|
emitter.onNext(progress / size)
|
||||||
|
} else {
|
||||||
|
emitter.onNext(-1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: EOFException) {
|
||||||
|
/*
|
||||||
|
This means we've finished downloading the file since sink.write
|
||||||
|
will throw an EOFException when the file to be read is empty.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Downloading $url failed. Status code: ${response.code}")
|
||||||
|
emitter.tryOnError(Exception())
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
Log.e(TAG, "Downloading $url failed.", ex)
|
||||||
|
downloadFile.deleteIfExists()
|
||||||
|
emitter.tryOnError(ex)
|
||||||
|
} finally {
|
||||||
|
source?.close()
|
||||||
|
sink.close()
|
||||||
|
if (emitter.isDisposed) {
|
||||||
|
downloadFile.deleteIfExists()
|
||||||
|
} else {
|
||||||
|
deleteOldVersions(context)
|
||||||
|
emitter.onComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the downloaded file, if it exists. Should be called when a download gets cancelled.
|
||||||
|
*/
|
||||||
|
fun deleteDownloadedFile(context: Context) {
|
||||||
|
getFontFile(context)?.deleteIfExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return display
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "EmojiCompatFont"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This String represents the sub-directory the fonts are stored in.
|
||||||
|
*/
|
||||||
|
private const val DIRECTORY = "emoji"
|
||||||
|
|
||||||
|
private const val CHUNK_SIZE = 4096L
|
||||||
|
|
||||||
|
// The system font gets some special behavior...
|
||||||
|
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<Int>, versionB: List<Int>): Int {
|
||||||
|
val len = max(versionB.size, versionA.size)
|
||||||
|
for (i in 0 until len) {
|
||||||
|
|
||||||
|
val vA = versionA.getOrElse(i) { 0 }
|
||||||
|
val vB = versionB.getOrElse(i) { 0 }
|
||||||
|
|
||||||
|
// It needs to be decided on the next level
|
||||||
|
if (vA == vB) continue
|
||||||
|
// Okay, is version B newer or version A?
|
||||||
|
return vA.compareTo(vB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The versions are equal
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is needed because when transparent compression is used OkHttp reports
|
||||||
|
* [ResponseBody.contentLength] as -1. We try to get the header which server sent
|
||||||
|
* us manually here.
|
||||||
|
*
|
||||||
|
* @see [OkHttp issue 259](https://github.com/square/okhttp/issues/259)
|
||||||
|
*/
|
||||||
|
private fun Response.length(): Long {
|
||||||
|
networkResponse?.let {
|
||||||
|
val header = it.header("Content-Length") ?: return -1
|
||||||
|
return header.toLongOrDefault(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case it's a fully cached response
|
||||||
|
return body?.contentLength() ?: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun File.deleteIfExists() {
|
||||||
|
if(exists() && !delete()) {
|
||||||
|
Log.e(TAG, "Could not delete file $this")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
android:id="@+id/activity_view_thread"
|
android:id="@+id/activity_view_thread"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context="com.keylesspalace.tusky.PreferencesActivity">
|
tools:context="com.keylesspalace.tusky.components.preference.PreferencesActivity">
|
||||||
|
|
||||||
<include layout="@layout/toolbar_basic" />
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,25 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
android:paddingTop="16dp">
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
<LinearLayout
|
<include
|
||||||
android:id="@+id/emoji_font_list"
|
android:id="@+id/item_blobmoji"
|
||||||
android:layout_width="match_parent"
|
layout="@layout/item_emoji_pref" />
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/item_blobmoji"
|
android:id="@+id/item_twemoji"
|
||||||
layout="@layout/item_emoji_pref" />
|
layout="@layout/item_emoji_pref" />
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/item_twemoji"
|
android:id="@+id/item_notoemoji"
|
||||||
layout="@layout/item_emoji_pref" />
|
layout="@layout/item_emoji_pref" />
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/item_notoemoji"
|
android:id="@+id/item_nomoji"
|
||||||
layout="@layout/item_emoji_pref" />
|
layout="@layout/item_emoji_pref" />
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/item_nomoji"
|
|
||||||
layout="@layout/item_emoji_pref" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/emoji_download_label"
|
android:id="@+id/emoji_download_label"
|
||||||
|
@ -39,9 +31,6 @@
|
||||||
android:paddingEnd="24dp"
|
android:paddingEnd="24dp"
|
||||||
android:paddingBottom="8dp"
|
android:paddingBottom="8dp"
|
||||||
android:text="@string/download_fonts"
|
android:text="@string/download_fonts"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/emoji_font_list" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</LinearLayout>
|
|
@ -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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue