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>
main
Constantin A 6 years ago committed by Konrad Pozniak
parent 81fa59515a
commit 1762e71218
  1. 4
      app/build.gradle
  2. 201
      app/src/main/assets/LICENSE_APACHE
  3. 18
      app/src/main/assets/about_emojicompat.html
  4. 42
      app/src/main/java/com/keylesspalace/tusky/AboutActivity.java
  5. 3
      app/src/main/java/com/keylesspalace/tusky/AccountActivity.java
  6. 278
      app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
  7. 23
      app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
  8. 322
      app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java
  9. 16
      app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt
  10. 4
      app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml
  11. 4
      app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml
  12. 11
      app/src/main/res/drawable/ic_blobmoji.xml
  13. 9
      app/src/main/res/drawable/ic_cancel_black_24dp.xml
  14. 9
      app/src/main/res/drawable/ic_twemoji.xml
  15. 63
      app/src/main/res/layout/about_emoji.xml
  16. 16
      app/src/main/res/layout/activity_about.xml
  17. 4
      app/src/main/res/layout/activity_account.xml
  18. 4
      app/src/main/res/layout/activity_compose.xml
  19. 2
      app/src/main/res/layout/activity_report.xml
  20. 38
      app/src/main/res/layout/dialog_emojicompat.xml
  21. 2
      app/src/main/res/layout/item_account.xml
  22. 4
      app/src/main/res/layout/item_autocomplete.xml
  23. 2
      app/src/main/res/layout/item_blocked_user.xml
  24. 118
      app/src/main/res/layout/item_emoji_pref.xml
  25. 2
      app/src/main/res/layout/item_follow.xml
  26. 2
      app/src/main/res/layout/item_follow_request.xml
  27. 2
      app/src/main/res/layout/item_muted_user.xml
  28. 2
      app/src/main/res/layout/item_saved_toot.xml
  29. 13
      app/src/main/res/layout/item_status.xml
  30. 18
      app/src/main/res/layout/item_status_detailed.xml
  31. 7
      app/src/main/res/layout/item_status_notification.xml
  32. 19
      app/src/main/res/values-de/strings.xml
  33. 19
      app/src/main/res/values/strings.xml
  34. 6
      app/src/main/res/xml/preferences.xml

@ -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'

@ -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.

@ -0,0 +1,18 @@
<html>
<body style="font-family: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif">
<h1>About these Emoji fonts</h1>
In order to display other emojis than your system's default set, you'll need to download additional emoji fonts.<br>
The fonts currently supported are:
<ul>
<li><em>Blobmoji</em><br>
This is a font based on the Blob emojis which have been used in stock Android from version 4.4 to 7.1.<br>
They are licensed under the Apache License 2.0 - you can get a copy <a href="LICENSE_APACHE.html">here</a>.<br>
<a href="https://github.com/c1710/blobmoji">Website</a>
</li>
<li><em>Twemoji</em>
This is the standard emoji set used by Masotodon. It has been developed by Twitter and is licensed under <a href="https://creativecommons.org/licenses/by/4.0/">CC-BY 4.0</a>.<br>
<a href="https://github.com/twitter/Twemoji">Website</a>
</li>
</ul>
</body>
</html>

@ -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());

@ -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
}
}

@ -0,0 +1,4 @@
<vector android:height="40dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M7,10l5,5 5,-5z"/>
</vector>

@ -0,0 +1,4 @@
<vector android:height="40dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M7,14l5,-5 5,5z"/>
</vector>

@ -0,0 +1,11 @@
<vector android:height="40dp" android:viewportHeight="128"
android:viewportWidth="128" android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FCC21B" android:pathData="M63.79,8.64C1.48,8.64 0,78.5 0,92.33c0,13.83 28.56,25.03 63.79,25.03c35.24,0 63.79,-11.21 63.79,-25.03C127.58,78.5 126.11,8.64 63.79,8.64z"/>
<path android:fillColor="#ED6C30" android:pathData="M96.46,70.26c-3.09,-2.09 -6.98,-0.71 -10.42,0c-9.58,1.98 -18,2.06 -22.04,2.06s-12.46,-0.08 -22.04,-2.06c-3.44,-0.71 -7.33,-2.09 -10.42,0c-3.92,2.65 -1.03,12.15 5.14,18.57c3.73,3.88 12.61,11.41 27.32,11.41c14.71,0 23.59,-7.53 27.32,-11.41C97.49,82.41 100.37,72.91 96.46,70.26z"/>
<path android:fillAlpha="0.82304526" android:fillColor="#34d400"
android:pathData="m64,40.154c-5.706,0.003 -20.717,-3.087 -30.093,-1.719 -9.376,1.368 -17.895,3.369 -24.948,5.915 -2.592,5.889 -4.435,11.954 -5.744,17.762 7.247,0.986 16.639,1.771 27.291,2.311 10.652,0.539 23.001,-1.063 34.849,-1.062 11.849,0.001 22.527,1.644 32.62,1.175 10.093,-0.47 19.163,-1.156 26.484,-2.026C123.126,56.491 121.215,50.184 118.5,44.076 111.438,41.604 102.976,39.666 93.698,38.345 84.419,37.024 69.706,40.151 64,40.154Z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.97736496"/>
<path android:fillAlpha="1" android:fillColor="#272727" android:pathData="M42.21,61.3c-4.49,0.04 -8.17,-4.27 -8.22,-9.62c-0.05,-5.37 3.55,-9.75 8.04,-9.79c4.48,-0.04 8.17,4.27 8.22,9.64C50.3,56.88 46.7,61.25 42.21,61.3z"/>
<path android:fillAlpha="1" android:fillColor="#272727" android:pathData="M86.32,61.3c4.48,-0.01 8.11,-4.36 8.1,-9.71c-0.01,-5.37 -3.66,-9.7 -8.14,-9.69c-4.49,0.01 -8.13,4.36 -8.12,9.73C78.18,56.98 81.83,61.31 86.32,61.3z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>

@ -0,0 +1,9 @@
<vector android:height="40dp" android:viewportHeight="36"
android:viewportWidth="36" android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFCB4C" android:pathData="M18,17.018m-17,0a17,17 0,1 1,34 0a17,17 0,1 1,-34 0"/>
<path android:fillColor="#65471B" android:pathData="M14.524,21.036c-0.145,-0.116 -0.258,-0.274 -0.312,-0.464 -0.134,-0.46 0.13,-0.918 0.59,-1.021 4.528,-1.021 7.577,1.363 7.706,1.465 0.384,0.306 0.459,0.845 0.173,1.205 -0.286,0.358 -0.828,0.401 -1.211,0.097 -0.11,-0.084 -2.523,-1.923 -6.182,-1.098 -0.274,0.061 -0.554,-0.016 -0.764,-0.184z"/>
<path android:fillColor="#65471B" android:pathData="M10.994,11.174a2.125,2.656 0,1 0,4.25 0a2.125,2.656 0,1 0,-4.25 0z"/>
<path android:fillColor="#65471B" android:pathData="M22.25,12.236a2.125,2.656 0,1 0,4.25 0a2.125,2.656 0,1 0,-4.25 0z"/>
<path android:fillColor="#F19020" android:pathData="M17.276,35.149s1.265,-0.411 1.429,-1.352c0.173,-0.972 -0.624,-1.167 -0.624,-1.167s1.041,-0.208 1.172,-1.376c0.123,-1.101 -0.861,-1.363 -0.861,-1.363s0.97,-0.4 1.016,-1.539c0.038,-0.959 -0.995,-1.428 -0.995,-1.428s5.038,-1.221 5.556,-1.341c0.516,-0.12 1.32,-0.615 1.069,-1.694 -0.249,-1.08 -1.204,-1.118 -1.697,-1.003 -0.494,0.115 -6.744,1.566 -8.9,2.068l-1.439,0.334c-0.54,0.127 -0.785,-0.11 -0.404,-0.512 0.508,-0.536 0.833,-1.129 0.946,-2.113 0.119,-1.035 -0.232,-2.313 -0.433,-2.809 -0.374,-0.921 -1.005,-1.649 -1.734,-1.899 -1.137,-0.39 -1.945,0.321 -1.542,1.561 0.604,1.854 0.208,3.375 -0.833,4.293 -2.449,2.157 -3.588,3.695 -2.83,6.973 0.828,3.575 4.377,5.876 7.952,5.048l3.152,-0.681z"/>
<path android:fillColor="#65471B" android:pathData="M9.296,6.351c-0.164,-0.088 -0.303,-0.224 -0.391,-0.399 -0.216,-0.428 -0.04,-0.927 0.393,-1.112 4.266,-1.831 7.699,-0.043 7.843,0.034 0.433,0.231 0.608,0.747 0.391,1.154 -0.216,0.405 -0.74,0.546 -1.173,0.318 -0.123,-0.063 -2.832,-1.432 -6.278,0.047 -0.257,0.109 -0.547,0.085 -0.785,-0.042zM21.431,10.101c-0.156,-0.098 -0.286,-0.243 -0.362,-0.424 -0.187,-0.442 0.023,-0.927 0.468,-1.084 4.381,-1.536 7.685,0.48 7.823,0.567 0.415,0.26 0.555,0.787 0.312,1.178 -0.242,0.39 -0.776,0.495 -1.191,0.238 -0.12,-0.072 -2.727,-1.621 -6.267,-0.379 -0.266,0.091 -0.553,0.046 -0.783,-0.096z"/>
</vector>

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="72dp">
<ImageButton
android:id="@+id/about_blobmoji_expand"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_drop_down_black_24dp"
android:tint="@color/colorPrimary"
app:layout_constraintVertical_bias="0.5"
android:focusable="true"
android:contentDescription="@android:string/cancel" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="72dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/license_blobmoji"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"/>
</android.support.constraint.ConstraintLayout>
<TextView
android:id="@+id/license_apache"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Apache license 2.0"
android:autoLink="web"
android:visibility="gone"
android:layout_margin="16dp"
android:textSize="14sp"
android:textColor="?android:textColorSecondary"/>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="88dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="72dp"
android:layout_marginEnd="16dp"
android:autoLink="web"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/license_twemoji"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"/>
</android.support.constraint.ConstraintLayout>
</merge>

@ -31,7 +31,7 @@
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textIsSelectable="true"
android:textStyle="bold" />
android:textStyle="bold"/>
<TextView
android:layout_width="match_parent"
@ -42,7 +42,7 @@
android:text="@string/about_tusky_license"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textIsSelectable="true" />
android:textIsSelectable="true"/>
<TextView
android:id="@+id/projectURL_TV"
@ -80,6 +80,18 @@
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@android:color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:lineSpacingMultiplier="1.2"
android:padding="@dimen/text_content_margin"
android:text="@string/about_emoji"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textIsSelectable="true" />
<include layout="@layout/about_emoji" />
</LinearLayout>
</ScrollView>

@ -80,7 +80,7 @@
app:layout_constraintEnd_toEndOf="@id/follow_btn"
app:layout_constraintTop_toBottomOf="@id/follow_btn" />
<TextView
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/account_display_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -116,7 +116,7 @@
app:srcCompat="@drawable/reblog_disabled_light"
tools:visibility="visible" />
<TextView
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/account_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -48,7 +48,7 @@
tools:text="Reply to @username"
tools:visibility="visible" />
<TextView
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/composeReplyContentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -70,7 +70,7 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
<android.support.text.emoji.widget.EmojiEditText
android:id="@+id/composeContentWarningField"
android:layout_width="match_parent"
android:layout_height="wrap_content"

@ -21,7 +21,7 @@
android:fadeScrollbars="false"
android:background="?attr/report_status_background_color" />
<EditText
<android.support.text.emoji.widget.EmojiEditText
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="20dp">
<LinearLayout
android:id="@+id/emoji_font_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/item_blobmoji"
layout="@layout/item_emoji_pref" />
<include
android:id="@+id/item_twemoji"
layout="@layout/item_emoji_pref"/>
<include
android:id="@+id/item_nomoji"
layout="@layout/item_emoji_pref" />
</LinearLayout>
<!--There's a short explanation that you'll need to download the emoji fonts first-->
<TextView
android:id="@+id/emoji_download_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/emoji_font_list"
android:paddingBottom="24dp"
android:paddingTop="20dp"
android:paddingEnd="24dp"
android:paddingStart="24dp"
android:text="@string/download_fonts"
android:textColor="?android:attr/textColorSecondary"/>
</android.support.constraint.ConstraintLayout>

@ -22,7 +22,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/account_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -22,7 +22,9 @@
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
<!--TODO: Check if this needs emoji support-->
<!--FIXME: The placeholder texts seem to be swapped-->
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -38,7 +38,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
<android.support.text.emoji.widget.EmojiTextView
android:id="@+id/blocked_user_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/emojicompat_container"
android:layout_width="match_parent"
android:layout_height="72dp"
android:background="?attr/selectableItemBackground"
xmlns:tools="http://schemas.android.com/tools">
<!--This is a thumbnail picture-->
<ImageView
android:id="@+id/emojicompat_thumb"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="4dp"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/ic_emoji_24dp"/>
<!--This is the font's name-->
<TextView
android:id="@+id/emojicompat_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
tools:text="@string/system_default"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--A short caption...-->
<TextView
android:id="@+id/emojicompat_caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="72dp"
tools:text=""
app:layout_constraintTop_toBottomOf="@id/emojicompat_name"
app:layout_constraintStart_toStartOf="@id/emojicompat_name"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintEnd_toEndOf="parent"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"/>
<!--This progress bar is shown while the font is downloading.-->
<ProgressBar
android:id="@+id/emojicompat_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="72dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"