EmojiCompat support (#600)
* Add EmojiCompat * EmojiCompat doesn' replace all emojis anymore * This app should be now capable of loading a EmojiCompat-font located in a file somewhere inside the device's storage * Should now replace all emojis * Add EmojiCompat support to EditTextTyped * Provide EmojiCompat fonts * The app won't crash anymore when no emoji font is available. Emoji font should now be located at [Private external app directory]/files/EmojiCompat.ttf * Removed BundledEmojiCompat dependency Since this EmojiCompat-implementation does not rely on BundledEmojiCompat, there's no reason to have it enabled. * Update EditTextTyped.kt Since connection isn't assigned to (I tried doing so), it can be declared final/val again. * Update README.md * Add some non-working emoji preferences * Add a short font list for testing * Finished implementation * Add Twemoji to font list * Update documentation, more comments * Delete AssetEmojiCompat which is obsolete now * Update the font list * Update the font list * Fix font list & add Exception handling for malformed JSON files (hopefully) * More fixes. It should work now... * Removed AssetEmojiCompat (again) * Add most of the changes * Improved the EmojiCompat dialog's style * The font list is now based on a static layout without external files * Re-add the real font URL for Twemoji * Emoji-font captions are now translatable * Removed one unused String (loading) * Removed emoji fonts from this repo * Applied changes from the PR change requests * The correct emoji font will be selected after cancelling a change * Add details on the EmojiCompat fonts available (not shown yet) * Add licensing information on Twemoji and Blobmoji * Reworked some strings * Moved FileEmojiCompat to its own library * Update FileEmojiCompat to the latest version (1.0.3) * EmojiCompat bug should be fixed * Better handling of failed downloads * Removed one TODO Signed-off-by: Constantin A <10349490+C1710@users.noreply.github.com> * Update emoji attribution strings Signed-off-by: Constantin A <10349490+C1710@users.noreply.github.com> * Fixed some misspelled strings Signed-off-by: Constantin A <10349490+C1710@users.noreply.github.com>
This commit is contained in:
parent
d9c481cf1c
commit
1108652823
34 changed files with 1253 additions and 34 deletions
|
@ -7,13 +7,19 @@ import android.support.design.widget.Snackbar;
|
|||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
@ -50,6 +56,7 @@ public class AboutActivity extends BaseActivity implements Injectable {
|
|||
|
||||
appAccountButton = findViewById(R.id.tusky_profile_button);
|
||||
appAccountButton.setOnClickListener(v -> onAccountButtonClick());
|
||||
setupAboutEmoji();
|
||||
}
|
||||
|
||||
private void onAccountButtonClick() {
|
||||
|
@ -109,4 +116,39 @@ public class AboutActivity extends BaseActivity implements Injectable {
|
|||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setupAboutEmoji() {
|
||||
// Inflate the TextView containing the Apache 2.0 license text.
|
||||
TextView apacheView = findViewById(R.id.license_apache);
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
InputStream apacheLicense = getAssets().open("LICENSE_APACHE");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
reader = new BufferedReader(
|
||||
new InputStreamReader(apacheLicense, "UTF-8"));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
builder.append('\n');
|
||||
}
|
||||
reader.close();
|
||||
apacheView.setText(builder);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Set up the button action
|
||||
ImageButton expand = findViewById(R.id.about_blobmoji_expand);
|
||||
expand.setOnClickListener(v ->
|
||||
{
|
||||
if(apacheView.getVisibility() == View.GONE) {
|
||||
apacheView.setVisibility(View.VISIBLE);
|
||||
((ImageButton) v).setImageResource(R.drawable.ic_arrow_drop_up_black_24dp);
|
||||
}
|
||||
else {
|
||||
apacheView.setVisibility(View.GONE);
|
||||
((ImageButton) v).setImageResource(R.drawable.ic_arrow_drop_down_black_24dp);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import android.support.design.widget.CollapsingToolbarLayout;
|
|||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.text.emoji.EmojiCompat;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
|
@ -302,7 +303,7 @@ public final class AccountActivity extends BottomSheetActivity implements Action
|
|||
displayName.setText(account.getName());
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(account.getName());
|
||||
getSupportActionBar().setTitle(EmojiCompat.get().process(account.getName()));
|
||||
|
||||
String subtitle = String.format(getString(R.string.status_username_format),
|
||||
account.getUsername());
|
||||
|
|
278
app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
Normal file
278
app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
Normal file
|
@ -0,0 +1,278 @@
|
|||
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.preference.DialogPreference;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
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 com.keylesspalace.tusky.util.EmojiCompatFont;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This Preference lets the user select their preferred emoji font
|
||||
*/
|
||||
public class EmojiPreference extends DialogPreference {
|
||||
private static final String TAG = "EmojiPreference";
|
||||
private final Context context;
|
||||
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};
|
||||
|
||||
private ArrayList<RadioButton> radioButtons = new ArrayList<>();
|
||||
|
||||
|
||||
public EmojiPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.context = context;
|
||||
|
||||
setDialogLayoutResource(R.layout.dialog_emojicompat);
|
||||
|
||||
setPositiveButtonText(android.R.string.ok);
|
||||
setNegativeButtonText(android.R.string.cancel);
|
||||
setDialogIcon(null);
|
||||
|
||||
// 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 onBindDialogView(View view) {
|
||||
super.onBindDialogView(view);
|
||||
for(int i = 0; i < viewIds.length; i++) {
|
||||
setupItem(view.findViewById(viewIds[i]), FONTS[i]);
|
||||
}
|
||||
}
|
||||
|
||||
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.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
|
||||
|
||||
font.downloadFont(context, 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(context)) {
|
||||
// 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);
|
||||
}
|
||||
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(context)
|
||||
.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)
|
||||
* @param positiveResult if OK has been selected.
|
||||
*/
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
if(positiveResult) {
|
||||
saveSelectedFont();
|
||||
if(selected != original) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.restart_required)
|
||||
.setMessage(R.string.restart_emoji)
|
||||
.setNegativeButton(R.string.later, null)
|
||||
.setPositiveButton(R.string.restart, ((dialog, which) -> {
|
||||
// Restart the app
|
||||
// TODO: I'm not sure if this is a good solution but it seems to work
|
||||
// From https://stackoverflow.com/a/17166729/5070653
|
||||
Intent launchIntent = new Intent(context, MainActivity.class);
|
||||
PendingIntent mPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
// This is the codepoint of the party face emoji :D
|
||||
0x1f973,
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
AlarmManager mgr =
|
||||
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
if (mgr != null) {
|
||||
mgr.set(
|
||||
AlarmManager.RTC,
|
||||
System.currentTimeMillis() + 100,
|
||||
mPendingIntent);
|
||||
}
|
||||
System.exit(0);
|
||||
})).show();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This line is needed in order to reset the radio buttons later
|
||||
selected = original;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,9 @@ import android.app.Service;
|
|||
import android.arch.persistence.room.Room;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.text.emoji.EmojiCompat;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
|
||||
import com.evernote.android.job.JobManager;
|
||||
|
@ -29,6 +31,7 @@ import com.jakewharton.picasso.OkHttp3Downloader;
|
|||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.db.AppDatabase;
|
||||
import com.keylesspalace.tusky.di.AppInjector;
|
||||
import com.keylesspalace.tusky.util.EmojiCompatFont;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
@ -86,13 +89,31 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
|||
|
||||
initAppInjector();
|
||||
initPicasso();
|
||||
initEmojiCompat();
|
||||
|
||||
JobManager.create(this).addJobCreator(notificationPullJobCreator);
|
||||
|
||||
//necessary for Android < APi 21
|
||||
//necessary for Android < API 21
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will load the EmojiCompat font which has been selected.
|
||||
* If this font does not work or if the user hasn't selected one (yet), it will use a
|
||||
* fallback solution instead which won't make any visible difference to using no EmojiCompat at all.
|
||||
*/
|
||||
private void initEmojiCompat() {
|
||||
int emojiSelection = PreferenceManager
|
||||
.getDefaultSharedPreferences(getApplicationContext())
|
||||
.getInt(EmojiPreference.FONT_PREFERENCE, 0);
|
||||
EmojiCompatFont font = EmojiCompatFont.byId(emojiSelection);
|
||||
// FileEmojiCompat will handle any non-existing font and provide a fallback solution.
|
||||
EmojiCompat.Config config = font.getConfig(getApplicationContext())
|
||||
// The user probably wants to get a consistent experience
|
||||
.setReplaceAll(true);
|
||||
EmojiCompat.init(config);
|
||||
}
|
||||
|
||||
protected void initAppInjector() {
|
||||
AppInjector.INSTANCE.init(this);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.c1710.filemojicompat.FileEmojiCompatConfig;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
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 {
|
||||
/**
|
||||
* 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, src;
|
||||
// The thumbnail image and the caption are provided as resource ids
|
||||
private final int img, caption;
|
||||
private AsyncTask fontDownloader;
|
||||
// The system font gets some special behavior...
|
||||
public static final EmojiCompatFont SYSTEM_DEFAULT =
|
||||
new EmojiCompatFont("system-default",
|
||||
"System Default",
|
||||
R.string.caption_systememoji,
|
||||
R.drawable.ic_emoji_24dp,
|
||||
"",
|
||||
"");
|
||||
private static final EmojiCompatFont BLOBMOJI =
|
||||
new EmojiCompatFont("Blobmoji",
|
||||
"Blobmoji",
|
||||
R.string.caption_blobmoji,
|
||||
R.drawable.ic_blobmoji,
|
||||
"https://tuskyapp.github.io/hosted/emoji/BlobmojiCompat.ttf",
|
||||
"https://github.com/c1710/blobmoji"
|
||||
);
|
||||
private static final EmojiCompatFont TWEMOJI =
|
||||
new EmojiCompatFont("Twemoji",
|
||||
"Twemoji",
|
||||
R.string.caption_twemoji,
|
||||
R.drawable.ic_twemoji,
|
||||
"https://tuskyapp.github.io/hosted/emoji/TwemojiCompat.ttf",
|
||||
"https://github.com/twitter/twemoji"
|
||||
);
|
||||
|
||||
/**
|
||||
* 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};
|
||||
|
||||
|
||||
private EmojiCompatFont(String name,
|
||||
String display,
|
||||
int caption,
|
||||
int img,
|
||||
String url,
|
||||
String src) {
|
||||
this.name = name;
|
||||
this.display = display;
|
||||
this.caption = caption;
|
||||
this.img = img;
|
||||
this.url = url;
|
||||
this.src = src;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 String getSrc() {
|
||||
return src;
|
||||
}
|
||||
|
||||
public Drawable getThumb(Context context) {
|
||||
return context.getResources().getDrawable(img);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will return the actual font file (regardless of its existence).
|
||||
* @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() + ".ttf");
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public FileEmojiCompatConfig getConfig(Context context) {
|
||||
return new FileEmojiCompatConfig(context, getFont(context));
|
||||
}
|
||||
|
||||
public boolean isDownloaded(Context context) {
|
||||
return this == SYSTEM_DEFAULT || getFont(context) != null && getFont(context).exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
fontDownloader = new Downloader(
|
||||
this,
|
||||
listeners)
|
||||
.execute(getFont(context));
|
||||
}
|
||||
else {
|
||||
for(Downloader.EmojiDownloadListener listener: listeners) {
|
||||
// The system emoji font is always downloaded...
|
||||
listener.onDownloaded(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 0;
|
||||
// Download!
|
||||
if (response.body() != null
|
||||
&& response.isSuccessful()
|
||||
&& (size = response.body().contentLength()) > 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?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return display;
|
||||
}
|
||||
}
|
|
@ -16,10 +16,12 @@
|
|||
package com.keylesspalace.tusky.view
|
||||
|
||||
import android.content.Context
|
||||
import android.support.text.emoji.widget.EmojiEditTextHelper
|
||||
import android.support.v13.view.inputmethod.EditorInfoCompat
|
||||
import android.support.v13.view.inputmethod.InputConnectionCompat
|
||||
import android.support.v7.widget.AppCompatMultiAutoCompleteTextView
|
||||
import android.text.InputType
|
||||
import android.text.method.KeyListener
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
|
@ -29,11 +31,17 @@ class EditTextTyped @JvmOverloads constructor(context: Context,
|
|||
: AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
|
||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||
|
||||
init {
|
||||
//fix a bug with autocomplete and some keyboards
|
||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||
inputType = newInputType
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
|
||||
}
|
||||
|
||||
override fun setKeyListener(input: KeyListener) {
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input))
|
||||
}
|
||||
|
||||
fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) {
|
||||
|
@ -44,10 +52,14 @@ class EditTextTyped @JvmOverloads constructor(context: Context,
|
|||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
return if (onCommitContentListener != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
InputConnectionCompat.createWrapper(connection, editorInfo,
|
||||
onCommitContentListener!!)
|
||||
getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo,
|
||||
onCommitContentListener!!), editorInfo)!!
|
||||
} else {
|
||||
connection
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmojiEditTextHelper(): EmojiEditTextHelper {
|
||||
return emojiEditTextHelper
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue