diff --git a/app/build.gradle b/app/build.gradle
index 91a1e653b..d8d7bb45c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -71,6 +71,10 @@ dependencies {
}
implementation 'com.evernote:android-job:1.2.5'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
+ // EmojiCompat
+ implementation "com.android.support:support-emoji:$supportLibraryVersion"
+ implementation "com.android.support:support-emoji-appcompat:$supportLibraryVersion"
+ implementation "de.c1710:filemojicompat:1.0.5"
//room
implementation 'android.arch.persistence.room:runtime:1.0.0'
kapt 'android.arch.persistence.room:compiler:1.0.0'
diff --git a/app/src/main/assets/LICENSE_APACHE b/app/src/main/assets/LICENSE_APACHE
new file mode 100644
index 000000000..8e66e5790
--- /dev/null
+++ b/app/src/main/assets/LICENSE_APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by
+the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems,
+and issue tracking systems that are managed by, or on behalf of, the
+Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the
+Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+(except as stated in this section) patent license to make, have made,
+use, offer to sell, sell, import, and otherwise transfer the Work,
+where such license applies only to those patent claims licensable
+by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s)
+with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work
+or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You
+meet the following conditions:
+
+(a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works
+that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work,
+excluding those notices that do not pertain to any part of
+the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained
+within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one
+of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or,
+within a display generated by the Derivative Works, if and
+wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and
+do not modify the License. You may add Your own attribution
+notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided
+that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions
+for use, reproduction, or distribution of Your modifications, or
+for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with
+the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work
+by You to the Licensor shall be under the terms and conditions of
+this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify
+the terms of any separate license agreement you may have executed
+with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+agreed to in writing, Licensor provides the Work (and each
+Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any
+risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise,
+unless required by applicable law (such as deliberate and grossly
+negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special,
+incidental, or consequential damages of any character arising as a
+result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer,
+and charge a fee for, acceptance of support, warranty, indemnity,
+or other liability obligations and/or rights consistent with this
+License. However, in accepting such obligations, You may act only
+on Your own behalf and on Your sole responsibility, not on behalf
+of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason
+of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following
+boilerplate notice, with the fields enclosed by brackets "[]"
+replaced with your own identifying information. (Don't include
+the brackets!) The text should be enclosed in the appropriate
+comment syntax for the file format. We also recommend that a
+file or class name and description of purpose be included on the
+same "printed page" as the copyright notice for easier
+identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/app/src/main/assets/about_emojicompat.html b/app/src/main/assets/about_emojicompat.html
new file mode 100644
index 000000000..43c5fd9e1
--- /dev/null
+++ b/app/src/main/assets/about_emojicompat.html
@@ -0,0 +1,18 @@
+
+
+ About these Emoji fonts
+ In order to display other emojis than your system's default set, you'll need to download additional emoji fonts.
+ The fonts currently supported are:
+
+ - Blobmoji
+ This is a font based on the Blob emojis which have been used in stock Android from version 4.4 to 7.1.
+ They are licensed under the Apache License 2.0 - you can get a copy here.
+ Website
+
+ - Twemoji
+ This is the standard emoji set used by Masotodon. It has been developed by Twitter and is licensed under CC-BY 4.0.
+ Website
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.java b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.java
index d0adb8c99..46df8d585 100644
--- a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.java
@@ -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);
+ }
+ });
+ }
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java
index 1000031d3..630f87e84 100644
--- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java
@@ -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());
diff --git a/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java b/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
new file mode 100644
index 000000000..0a682ec6d
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
@@ -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 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;
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
index 7596a993e..11628516d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
+++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
@@ -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);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java
new file mode 100644
index 000000000..25054ee29
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java
@@ -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 {
+ // 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;
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt
index fb837b153..4160f8b20 100644
--- a/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt
@@ -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
+ }
}
diff --git a/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml
new file mode 100644
index 000000000..88a73064b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml
new file mode 100644
index 000000000..49be73b78
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_blobmoji.xml b/app/src/main/res/drawable/ic_blobmoji.xml
new file mode 100644
index 000000000..2d475710d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_blobmoji.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_cancel_black_24dp.xml b/app/src/main/res/drawable/ic_cancel_black_24dp.xml
new file mode 100644
index 000000000..7d2b57eb2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cancel_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_twemoji.xml b/app/src/main/res/drawable/ic_twemoji.xml
new file mode 100644
index 000000000..0888d26fb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_twemoji.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/about_emoji.xml b/app/src/main/res/layout/about_emoji.xml
new file mode 100644
index 000000000..8dbeba250
--- /dev/null
+++ b/app/src/main/res/layout/about_emoji.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
index f25eaaa4c..53beddb07 100644
--- a/app/src/main/res/layout/activity_about.xml
+++ b/app/src/main/res/layout/activity_about.xml
@@ -31,7 +31,7 @@
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textIsSelectable="true"
- android:textStyle="bold" />
+ android:textStyle="bold"/>
+ android:textIsSelectable="true"/>
+
+
+
diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml
index 5c2efaf3d..140c779cf 100644
--- a/app/src/main/res/layout/activity_account.xml
+++ b/app/src/main/res/layout/activity_account.xml
@@ -80,7 +80,7 @@
app:layout_constraintEnd_toEndOf="@id/follow_btn"
app:layout_constraintTop_toBottomOf="@id/follow_btn" />
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml
index d3de75fdb..2a0fd16f1 100644
--- a/app/src/main/res/layout/item_account.xml
+++ b/app/src/main/res/layout/item_account.xml
@@ -22,7 +22,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_follow.xml b/app/src/main/res/layout/item_follow.xml
index dc8f695f8..d3a19338f 100644
--- a/app/src/main/res/layout/item_follow.xml
+++ b/app/src/main/res/layout/item_follow.xml
@@ -38,7 +38,7 @@
android:scaleType="fitCenter"
android:textSize="?attr/status_text_medium" />
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
+
-
+
-
+
+
-
-
- Alle Beiträge aus/einklappen
Antworten
Nachschlagen…
+ Emoji-Stil
+ System-Standard
+ Du musst diese Emoji-Sets zunächst herunterladen.
+ App-Neustart erforderlich
+ Du musst Tusky neustarten um die Änderungen anzuwenden
+ Später
+ Neustarten
+ Die Standard-Emojis deines Geräts
+ Ein Emoji–Set, das auf den "Blob"–Emojis aus Android 4.4–7.1 basiert
+ Die Standard-Emojis von Mastodon
+
+ Tusky benutzt die folgenden Emoji-Schriftarten:\n
+ Twemoji von Twitter - https://github.com/twitter/Twemoji\n
+ Blobmoji von C1710, basierend auf Noto-Emoji - https://github.com/c1710/Blobmoji\n
+ Twemoji wurde auf die Nutzung mit der EmojiCompat-Bibliothek angepasst.
+
+ Blobmoji ist lizenziert nach der Apache 2.0-Lizenz
+ Twemoji ist als CC-BY-4.0 lizenziert - https://creativecommons.org/licenses/by/4.0/
+ Download fehlgeschlagen.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c3bcb8b0f..92da96ccc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -310,7 +310,26 @@
Your instance %s does not have any custom emojis
Copied to clipboard
+ Emoji style
+ System default
+ You\'ll need to download these emoji sets first
Performing lookup...
Expand/Collapse all statuses
+ App restart required
+ You\'ll need to restart Tusky in order to apply these changes
+ Later
+ Restart
+ Your device\'s default emoji set
+ An emoji set based on the Blob emojis known from Android 4.4–7.1
+ Mastodon\'s standard emoji set
+
+ Tusky uses the following emoji sets:\n
+ Twemoji by Twitter - https://github.com/twitter/Twemoji\n
+ Blobmoji by C1710, based on Noto-Emoji - https://github.com/c1710/Blobmoji\n
+ Twemoji has been modified in order to use it with the EmojiCompat library
+
+ Blobmoji is licensed under the Apache license 2.0
+ Twemoji is licensed as CC-BY 4.0 - https://creativecommons.org/licenses/by/4.0/
+ Download failed
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 504d05b13..1dd472b85 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -12,6 +12,12 @@
android:summary="%s"
android:title="@string/pref_title_app_theme" />
+
+