Emoji: minor bug fixes, code cleanup, logging, visibility,

This commit is contained in:
akwizgran
2016-09-23 16:48:07 +01:00
parent d5beca5351
commit c917110e6a
18 changed files with 205 additions and 205 deletions

View File

@@ -10,6 +10,6 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="org.briarproject.android.util.BriarRecyclerViewBehavior"/> app:layout_behavior="org.briarproject.android.view.BriarRecyclerViewBehavior"/>
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>

View File

@@ -1,33 +1,14 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.components; package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
@@ -37,15 +18,23 @@ import org.briarproject.R;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger;
import static android.content.Context.WINDOW_SERVICE;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
/** /**
* RelativeLayout that, when a view container, will report back when it thinks a soft keyboard * RelativeLayout that, when a view container, will report back when it thinks
* has been opened and what its height would be. * a soft keyboard has been opened and what its height would be.
*/ */
@UiThread @UiThread
public class KeyboardAwareRelativeLayout extends RelativeLayout { public class KeyboardAwareRelativeLayout extends RelativeLayout {
private static final String TAG =
KeyboardAwareRelativeLayout.class.getSimpleName(); private static final Logger LOG =
Logger.getLogger(KeyboardAwareRelativeLayout.class.getName());
private final Rect rect = new Rect(); private final Rect rect = new Rect();
private final Set<OnKeyboardHiddenListener> hiddenListeners = private final Set<OnKeyboardHiddenListener> hiddenListeners =
@@ -100,7 +89,7 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
int oldRotation = rotation; int oldRotation = rotation;
rotation = getDeviceRotation(); rotation = getDeviceRotation();
if (oldRotation != rotation) { if (oldRotation != rotation) {
Log.w(TAG, "rotation changed"); LOG.info("Rotation changed");
onKeyboardClose(); onKeyboardClose();
} }
} }
@@ -111,13 +100,13 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
return; return;
} }
if (viewInset == 0 && Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) if (viewInset == 0 && Build.VERSION.SDK_INT >= 21)
viewInset = getViewInset(); viewInset = getViewInset();
final int availableHeight = int availableHeight =
this.getRootView().getHeight() - statusBarHeight - viewInset; getRootView().getHeight() - statusBarHeight - viewInset;
getWindowVisibleDisplayFrame(rect); getWindowVisibleDisplayFrame(rect);
final int keyboardHeight = availableHeight - (rect.bottom - rect.top); int keyboardHeight = availableHeight - (rect.bottom - rect.top);
if (keyboardHeight > minKeyboardSize) { if (keyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != keyboardHeight) if (getKeyboardHeight() != keyboardHeight)
@@ -128,7 +117,7 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
} }
} }
@TargetApi(VERSION_CODES.LOLLIPOP) @TargetApi(21)
private int getViewInset() { private int getViewInset() {
try { try {
Field attachInfoField = View.class.getDeclaredField("mAttachInfo"); Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
@@ -141,25 +130,26 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
Rect insets = (Rect) stableInsetsField.get(attachInfo); Rect insets = (Rect) stableInsetsField.get(attachInfo);
return insets.bottom; return insets.bottom;
} }
} catch (NoSuchFieldException nsfe) { } catch (NoSuchFieldException e) {
Log.w(TAG, "field reflection error when measuring view inset", LOG.log(WARNING,
nsfe); "field reflection error when measuring view inset", e);
} catch (IllegalAccessException iae) { } catch (IllegalAccessException e) {
Log.w(TAG, "access reflection error when measuring view inset", LOG.log(WARNING,
iae); "access reflection error when measuring view inset", e);
} }
return 0; return 0;
} }
protected void onKeyboardOpen(int keyboardHeight) { protected void onKeyboardOpen(int keyboardHeight) {
Log.w(TAG, "onKeyboardOpen(" + keyboardHeight + ")"); if (LOG.isLoggable(INFO))
LOG.info("onKeyboardOpen(" + keyboardHeight + ")");
keyboardOpen = true; keyboardOpen = true;
notifyShownListeners(); notifyShownListeners();
} }
protected void onKeyboardClose() { protected void onKeyboardClose() {
Log.w(TAG, "onKeyboardClose()"); LOG.info("onKeyboardClose()");
keyboardOpen = false; keyboardOpen = false;
notifyHiddenListeners(); notifyHiddenListeners();
} }
@@ -175,13 +165,12 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
public boolean isLandscape() { public boolean isLandscape() {
int rotation = getDeviceRotation(); int rotation = getDeviceRotation();
return rotation == Surface.ROTATION_90 || return rotation == ROTATION_90 || rotation == ROTATION_270;
rotation == Surface.ROTATION_270;
} }
private int getDeviceRotation() { private int getDeviceRotation() {
WindowManager windowManager = (WindowManager) getContext() WindowManager windowManager =
.getSystemService(Activity.WINDOW_SERVICE); (WindowManager) getContext().getSystemService(WINDOW_SERVICE);
return windowManager.getDefaultDisplay().getRotation(); return windowManager.getDefaultDisplay().getRotation();
} }
@@ -190,10 +179,10 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
} }
private int getKeyboardPortraitHeight() { private int getKeyboardPortraitHeight() {
int keyboardHeight = SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext()) PreferenceManager.getDefaultSharedPreferences(getContext());
.getInt("keyboard_height_portrait", int keyboardHeight = prefs.getInt("keyboard_height_portrait",
defaultCustomKeyboardSize); defaultCustomKeyboardSize);
return clamp(keyboardHeight, minCustomKeyboardSize, return clamp(keyboardHeight, minCustomKeyboardSize,
getRootView().getHeight() - minCustomKeyboardTopMargin); getRootView().getHeight() - minCustomKeyboardTopMargin);
} }
@@ -203,8 +192,9 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
} }
private void setKeyboardPortraitHeight(int height) { private void setKeyboardPortraitHeight(int height) {
PreferenceManager.getDefaultSharedPreferences(getContext()) SharedPreferences prefs =
.edit().putInt("keyboard_height_portrait", height).apply(); PreferenceManager.getDefaultSharedPreferences(getContext());
prefs.edit().putInt("keyboard_height_portrait", height).apply();
} }
public void postOnKeyboardClose(final Runnable runnable) { public void postOnKeyboardClose(final Runnable runnable) {
@@ -254,7 +244,8 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
} }
private void notifyHiddenListeners() { private void notifyHiddenListeners() {
final Set<OnKeyboardHiddenListener> listeners = // Make a copy as listeners may remove themselves when called
Set<OnKeyboardHiddenListener> listeners =
new HashSet<>(hiddenListeners); new HashSet<>(hiddenListeners);
for (OnKeyboardHiddenListener listener : listeners) { for (OnKeyboardHiddenListener listener : listeners) {
listener.onKeyboardHidden(); listener.onKeyboardHidden();
@@ -262,8 +253,8 @@ public class KeyboardAwareRelativeLayout extends RelativeLayout {
} }
private void notifyShownListeners() { private void notifyShownListeners() {
final Set<OnKeyboardShownListener> listeners = // Make a copy as listeners may remove themselves when called
new HashSet<>(shownListeners); Set<OnKeyboardShownListener> listeners = new HashSet<>(shownListeners);
for (OnKeyboardShownListener listener : listeners) { for (OnKeyboardShownListener listener : listeners) {
listener.onKeyboardShown(); listener.onKeyboardShown();
} }

View File

@@ -1,16 +1,18 @@
package org.thoughtcrime.securesms.components; package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import android.widget.ImageButton; import android.widget.ImageButton;
import static android.view.HapticFeedbackConstants.KEYBOARD_TAP;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
@UiThread @UiThread
public class RepeatableImageKey extends ImageButton { public class RepeatableImageKey extends ImageButton {
@@ -42,7 +44,7 @@ public class RepeatableImageKey extends ImageButton {
} }
private void notifyListener() { private void notifyListener() {
if (this.listener != null) this.listener.onKeyEvent(); if (listener != null) listener.onKeyEvent();
} }
private class RepeaterClickListener implements OnClickListener { private class RepeaterClickListener implements OnClickListener {
@@ -56,31 +58,28 @@ public class RepeatableImageKey extends ImageButton {
@Override @Override
public void run() { public void run() {
notifyListener(); notifyListener();
postDelayed(this, VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1 postDelayed(this, ViewConfiguration.getKeyRepeatDelay());
? ViewConfiguration.getKeyRepeatDelay()
: 50);
} }
} }
private class RepeaterTouchListener implements OnTouchListener { private class RepeaterTouchListener implements OnTouchListener {
private Repeater repeater;
private final Repeater repeater;
private RepeaterTouchListener() { private RepeaterTouchListener() {
this.repeater = new Repeater(); repeater = new Repeater();
} }
@Override @Override
public boolean onTouch(View view, MotionEvent motionEvent) { public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) { switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN: case ACTION_DOWN:
view.postDelayed(repeater, view.postDelayed(repeater,
VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1 ViewConfiguration.getKeyRepeatTimeout());
? ViewConfiguration.getKeyRepeatTimeout() performHapticFeedback(KEYBOARD_TAP);
: ViewConfiguration.getLongPressTimeout());
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
return false; return false;
case MotionEvent.ACTION_CANCEL: case ACTION_CANCEL:
case MotionEvent.ACTION_UP: case ACTION_UP:
view.removeCallbacks(repeater); view.removeCallbacks(repeater);
return false; return false;
default: default:

View File

@@ -6,8 +6,9 @@ import android.support.annotation.UiThread;
import android.text.style.ImageSpan; import android.text.style.ImageSpan;
@UiThread @UiThread
public class AnimatingImageSpan extends ImageSpan { class AnimatingImageSpan extends ImageSpan {
public AnimatingImageSpan(Drawable drawable, Callback callback) {
AnimatingImageSpan(Drawable drawable, Callback callback) {
super(drawable, ALIGN_BOTTOM); super(drawable, ALIGN_BOTTOM);
drawable.setCallback(callback); drawable.setCallback(callback);
} }

View File

@@ -12,7 +12,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import com.astuetz.PagerSlidingTabStrip; import com.astuetz.PagerSlidingTabStrip;
@@ -27,15 +26,18 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_DEL;
import static android.widget.ImageView.ScaleType.CENTER_INSIDE;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
@UiThread @UiThread
public class EmojiDrawer extends LinearLayout { public class EmojiDrawer extends LinearLayout {
private static final String TAG = EmojiDrawer.class.getName(); private static final Logger LOG =
private static final Logger LOG = Logger.getLogger(TAG); Logger.getLogger(EmojiDrawer.class.getName());
private static final KeyEvent DELETE_KEY_EVENT = private static final KeyEvent DELETE_KEY_EVENT =
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); new KeyEvent(ACTION_DOWN, KEYCODE_DEL);
private ViewPager pager; private ViewPager pager;
private List<EmojiPageModel> models; private List<EmojiPageModel> models;
@@ -70,7 +72,6 @@ public class EmojiDrawer extends LinearLayout {
} }
private void initializeResources(View v) { private void initializeResources(View v) {
LOG.info("initializeResources()");
this.pager = (ViewPager) v.findViewById(R.id.emoji_pager); this.pager = (ViewPager) v.findViewById(R.id.emoji_pager);
this.strip = (PagerSlidingTabStrip) v.findViewById(R.id.tabs); this.strip = (PagerSlidingTabStrip) v.findViewById(R.id.tabs);
@@ -93,7 +94,7 @@ public class EmojiDrawer extends LinearLayout {
ViewGroup.LayoutParams params = getLayoutParams(); ViewGroup.LayoutParams params = getLayoutParams();
params.height = height; params.height = height;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("showing emoji drawer with height " + params.height); LOG.info("Showing emoji drawer with height " + params.height);
setLayoutParams(params); setLayoutParams(params);
setVisibility(VISIBLE); setVisibility(VISIBLE);
if (drawerListener != null) drawerListener.onShown(); if (drawerListener != null) drawerListener.onShown();
@@ -102,7 +103,6 @@ public class EmojiDrawer extends LinearLayout {
public void hide() { public void hide() {
setVisibility(GONE); setVisibility(GONE);
if (drawerListener != null) drawerListener.onHidden(); if (drawerListener != null) drawerListener.onHidden();
LOG.info("hide()");
} }
private void initializeEmojiGrid() { private void initializeEmojiGrid() {
@@ -111,7 +111,6 @@ public class EmojiDrawer extends LinearLayout {
new EmojiSelectionListener() { new EmojiSelectionListener() {
@Override @Override
public void onEmojiSelected(String emoji) { public void onEmojiSelected(String emoji) {
LOG.info("onEmojiSelected()");
recentModel.onCodePointSelected(emoji); recentModel.onCodePointSelected(emoji);
if (listener != null) listener.onEmojiSelected(emoji); if (listener != null) listener.onEmojiSelected(emoji);
} }
@@ -174,7 +173,7 @@ public class EmojiDrawer extends LinearLayout {
@Override @Override
public View getCustomTabView(ViewGroup viewGroup, int i) { public View getCustomTabView(ViewGroup viewGroup, int i) {
ImageView image = new ImageView(context); ImageView image = new ImageView(context);
image.setScaleType(ScaleType.CENTER_INSIDE); image.setScaleType(CENTER_INSIDE);
image.setImageResource(pages.get(i).getIcon()); image.setImageResource(pages.get(i).getIcon());
return image; return image;
} }

View File

@@ -9,7 +9,7 @@ import android.text.TextUtils;
import android.widget.TextView; import android.widget.TextView;
@UiThread @UiThread
public class EmojiFilter implements InputFilter { class EmojiFilter implements InputFilter {
private final TextView view; private final TextView view;

View File

@@ -4,7 +4,8 @@ import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
public interface EmojiPageModel { interface EmojiPageModel {
@DrawableRes @DrawableRes
int getIcon(); int getIcon();

View File

@@ -20,6 +20,7 @@ import org.briarproject.R;
public class EmojiPageView extends FrameLayout { public class EmojiPageView extends FrameLayout {
private final GridView grid; private final GridView grid;
private EmojiSelectionListener listener; private EmojiSelectionListener listener;
public EmojiPageView(Context context) { public EmojiPageView(Context context) {
@@ -60,15 +61,15 @@ public class EmojiPageView extends FrameLayout {
private static class EmojiGridAdapter extends BaseAdapter { private static class EmojiGridAdapter extends BaseAdapter {
protected final Context context; private final Context context;
private final int emojiSize;
private final EmojiPageModel model; private final EmojiPageModel model;
private final int emojiSize;
private EmojiGridAdapter(Context context, EmojiPageModel model) { private EmojiGridAdapter(Context context, EmojiPageModel model) {
this.context = context; this.context = context;
this.emojiSize = (int) context.getResources()
.getDimension(R.dimen.emoji_drawer_size);
this.model = model; this.model = model;
emojiSize = (int) context.getResources()
.getDimension(R.dimen.emoji_drawer_size);
} }
@Override @Override
@@ -88,15 +89,14 @@ public class EmojiPageView extends FrameLayout {
} }
@Override @Override
public View getView(final int position, final View convertView, public View getView(int position, View convertView, ViewGroup parent) {
final ViewGroup parent) { EmojiView view;
final EmojiView view; int pad = context.getResources()
final int pad = context.getResources()
.getDimensionPixelSize(R.dimen.emoji_drawer_item_padding); .getDimensionPixelSize(R.dimen.emoji_drawer_item_padding);
if (convertView != null && convertView instanceof EmojiView) { if (convertView != null && convertView instanceof EmojiView) {
view = (EmojiView) convertView; view = (EmojiView) convertView;
} else { } else {
final EmojiView emojiView = new EmojiView(context); EmojiView emojiView = new EmojiView(context);
emojiView.setPadding(pad, pad, pad, pad); emojiView.setPadding(pad, pad, pad, pad);
emojiView.setLayoutParams( emojiView.setLayoutParams(
new AbsListView.LayoutParams(emojiSize + 2 * pad, new AbsListView.LayoutParams(emojiSize + 2 * pad,
@@ -109,7 +109,7 @@ public class EmojiPageView extends FrameLayout {
} }
} }
public interface EmojiSelectionListener { interface EmojiSelectionListener {
void onEmojiSelected(String emoji); void onEmojiSelected(String emoji);
} }
} }

View File

@@ -8,12 +8,14 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
class EmojiPages { class EmojiPages {
static List<EmojiPageModel> getPages(Context ctx) { static List<EmojiPageModel> getPages(Context ctx) {
return Arrays.<EmojiPageModel>asList( return Arrays.<EmojiPageModel>asList(
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_smiley_people, new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_smiley_people,
R.array.emoji_smiley_people, R.array.emoji_smiley_people,
"emoji_smiley_people.png"), "emoji_smiley_people.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_animals_nature, new StaticEmojiPageModel(ctx,
R.drawable.ic_emoji_animals_nature,
R.array.emoji_animals_nature, R.array.emoji_animals_nature,
"emoji_animals_nature.png"), "emoji_animals_nature.png"),
new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_food_drink, new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_food_drink,

View File

@@ -5,7 +5,6 @@ import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.AsyncTask; import android.os.AsyncTask;
@@ -34,25 +33,30 @@ import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
import static android.graphics.PixelFormat.TRANSLUCENT;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
public class EmojiProvider { public class EmojiProvider {
private static final String TAG = EmojiProvider.class.getSimpleName();
private static volatile EmojiProvider instance = null; private static volatile EmojiProvider INSTANCE = null;
private static final Paint paint =
private static final Paint PAINT =
new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
@Inject @Inject
AndroidExecutor androidExecutor; AndroidExecutor androidExecutor;
private static final Logger LOG = Logger.getLogger(TAG); private static final Logger LOG =
Logger.getLogger(EmojiProvider.class.getName());
private final SparseArray<DrawInfo> offsets = new SparseArray<>(); private final SparseArray<DrawInfo> offsets = new SparseArray<>();
private static final Pattern EMOJI_RANGE = Pattern.compile( private static final Pattern EMOJI_RANGE = Pattern.compile(
// 0x203c,0x2049 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee // 0x203c,0x2049 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee
// |=== !!, ?! ===||==== misc ===||========= emoticons =======||========== flags ==========| // |=== !!, ?! ===||==== misc ===||========= emoticons =======||========== flags ==========|
"[\\u203c\\u2049\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83f\\udfff\\udbb9\\udce5-\\udbb9\\udcee]"); "[\\u203c\\u2049\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83f\\udfff\\udbb9\\udce5-\\udbb9\\udcee]");
private static final int EMOJI_RAW_HEIGHT = 64; private static final int EMOJI_RAW_HEIGHT = 64;
private static final int EMOJI_RAW_WIDTH = 64; private static final int EMOJI_RAW_WIDTH = 64;
@@ -60,30 +64,29 @@ public class EmojiProvider {
private static final int EMOJI_PER_ROW = 32; private static final int EMOJI_PER_ROW = 32;
private final Context context; private final Context context;
private final float decodeScale; private final float decodeScale, verticalPad;
private final float verticalPad;
private final List<EmojiPageModel> staticPages; private final List<EmojiPageModel> staticPages;
public static EmojiProvider getInstance(Context context) { static EmojiProvider getInstance(Context context) {
if (instance == null) { if (INSTANCE == null) {
synchronized (EmojiProvider.class) { synchronized (EmojiProvider.class) {
if (instance == null) { if (INSTANCE == null) {
LOG.info("Creating new instance of EmojiProvider"); LOG.info("Creating new instance of EmojiProvider");
instance = new EmojiProvider(context); INSTANCE = new EmojiProvider(context);
((BaseActivity) context).getActivityComponent() ((BaseActivity) context).getActivityComponent()
.inject(instance); .inject(INSTANCE);
} }
} }
} }
return instance; return INSTANCE;
} }
private EmojiProvider(Context context) { private EmojiProvider(Context context) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.decodeScale = Math.min(1f, float drawerSize =
context.getResources().getDimension(R.dimen.emoji_drawer_size) / context.getResources().getDimension(R.dimen.emoji_drawer_size);
EMOJI_RAW_HEIGHT); decodeScale = Math.min(1f, drawerSize / EMOJI_RAW_HEIGHT);
this.verticalPad = EMOJI_VERT_PAD * this.decodeScale; verticalPad = EMOJI_VERT_PAD * this.decodeScale;
staticPages = EmojiPages.getPages(context); staticPages = EmojiPages.getPages(context);
for (EmojiPageModel page : staticPages) { for (EmojiPageModel page : staticPages) {
if (page.hasSpriteMap()) { if (page.hasSpriteMap()) {
@@ -97,7 +100,8 @@ public class EmojiProvider {
} }
@Nullable @Nullable
public Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) { Spannable emojify(@Nullable CharSequence text,
@NonNull TextView tv) {
if (text == null) return null; if (text == null) return null;
Matcher matches = EMOJI_RANGE.matcher(text); Matcher matches = EMOJI_RANGE.matcher(text);
SpannableStringBuilder builder = new SpannableStringBuilder(text); SpannableStringBuilder builder = new SpannableStringBuilder(text);
@@ -107,15 +111,14 @@ public class EmojiProvider {
Drawable drawable = getEmojiDrawable(codePoint); Drawable drawable = getEmojiDrawable(codePoint);
if (drawable != null) { if (drawable != null) {
builder.setSpan(new EmojiSpan(drawable, tv), matches.start(), builder.setSpan(new EmojiSpan(drawable, tv), matches.start(),
matches.end(), matches.end(), SPAN_EXCLUSIVE_EXCLUSIVE);
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
return builder; return builder;
} }
@Nullable @Nullable
public Drawable getEmojiDrawable(int emojiCode) { Drawable getEmojiDrawable(int emojiCode) {
return getEmojiDrawable(offsets.get(emojiCode)); return getEmojiDrawable(offsets.get(emojiCode));
} }
@@ -139,22 +142,30 @@ public class EmojiProvider {
@Override @Override
public void onFailure(Throwable error) { public void onFailure(Throwable error) {
LOG.log(WARNING, error.toString(), error); if (LOG.isLoggable(WARNING))
LOG.log(WARNING, error.toString(), error);
} }
}); });
return drawable; return drawable;
} }
public List<EmojiPageModel> getStaticPages() { List<EmojiPageModel> getStaticPages() {
return staticPages; return staticPages;
} }
public class EmojiDrawable extends Drawable { class EmojiDrawable extends Drawable {
private final DrawInfo info; private final DrawInfo info;
private final float intrinsicWidth, intrinsicHeight;
private Bitmap bmp; private Bitmap bmp;
private float intrinsicWidth;
private float intrinsicHeight; private EmojiDrawable(DrawInfo info, float decodeScale) {
this.info = info;
intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
}
@Override @Override
public int getIntrinsicWidth() { public int getIntrinsicWidth() {
@@ -166,32 +177,25 @@ public class EmojiProvider {
return (int) intrinsicHeight; return (int) intrinsicHeight;
} }
private EmojiDrawable(DrawInfo info, float decodeScale) {
this.info = info;
this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
}
@Override @Override
public void draw(@NonNull Canvas canvas) { public void draw(@NonNull Canvas canvas) {
if (bmp == null) { if (bmp == null) {
return; return;
} }
final int row = info.index / EMOJI_PER_ROW; int row = info.index / EMOJI_PER_ROW;
final int row_index = info.index % EMOJI_PER_ROW; int rowIndex = info.index % EMOJI_PER_ROW;
canvas.drawBitmap(bmp, int left = (int) (rowIndex * intrinsicWidth);
new Rect((int) (row_index * intrinsicWidth), int top = (int) (row * intrinsicHeight + row * verticalPad);
(int) (row * intrinsicHeight + row * verticalPad), int right = (int) ((rowIndex + 1) * intrinsicWidth);
(int) ((row_index + 1) * intrinsicWidth), int bottom =
(int) ((row + 1) * intrinsicHeight + (int) ((row + 1) * intrinsicHeight + row * verticalPad);
row * verticalPad)), canvas.drawBitmap(bmp, new Rect(left, top, right, bottom),
getBounds(), getBounds(), PAINT);
paint);
} }
public void setBitmap(Bitmap bitmap) { void setBitmap(Bitmap bitmap) {
if (bmp == null || !bmp.sameAs(bitmap)) { if (bmp == null || !bmp.sameAs(bitmap)) {
bmp = bitmap; bmp = bitmap;
invalidateSelf(); invalidateSelf();
@@ -200,7 +204,7 @@ public class EmojiProvider {
@Override @Override
public int getOpacity() { public int getOpacity() {
return PixelFormat.TRANSLUCENT; return TRANSLUCENT;
} }
@Override @Override
@@ -214,26 +218,29 @@ public class EmojiProvider {
private static class DrawInfo { private static class DrawInfo {
private EmojiPageBitmap page;
int index;
private DrawInfo(final EmojiPageBitmap page, final int index) { private final EmojiPageBitmap page;
private final int index;
private DrawInfo(EmojiPageBitmap page, int index) {
this.page = page; this.page = page;
this.index = index; this.index = index;
} }
@Override @Override
public String toString() { public String toString() {
return "DrawInfo{ " +"page = " + page +", index = " + index + '}'; return "DrawInfo{ " + "page = " + page + ", index = " + index + '}';
} }
} }
private class EmojiPageBitmap { private class EmojiPageBitmap {
private EmojiPageModel model;
private SoftReference<Bitmap> bitmapReference; private final EmojiPageModel model;
private ListenableFutureTask<Bitmap> task; private ListenableFutureTask<Bitmap> task;
private volatile SoftReference<Bitmap> bitmapReference;
private EmojiPageBitmap(EmojiPageModel model) { private EmojiPageBitmap(EmojiPageModel model) {
this.model = model; this.model = model;
} }
@@ -249,7 +256,8 @@ public class EmojiProvider {
@Nullable @Nullable
public Bitmap call() throws Exception { public Bitmap call() throws Exception {
try { try {
LOG.info("loading page " + model.getSprite()); if (LOG.isLoggable(INFO))
LOG.info("Loading page " + model.getSprite());
return loadPage(); return loadPage();
} catch (IOException ioe) { } catch (IOException ioe) {
LOG.log(WARNING, ioe.toString(), ioe); LOG.log(WARNING, ioe.toString(), ioe);
@@ -283,7 +291,8 @@ public class EmojiProvider {
"file:///android_asset/" + model.getSprite(), "file:///android_asset/" + model.getSprite(),
decodeScale); decodeScale);
bitmapReference = new SoftReference<>(bitmap); bitmapReference = new SoftReference<>(bitmap);
LOG.info("onPageLoaded(" + model.getSprite() + ")"); if (LOG.isLoggable(INFO))
LOG.info("Loaded page " + model.getSprite());
return bitmap; return bitmap;
} catch (BitmapDecodingException e) { } catch (BitmapDecodingException e) {
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);

View File

@@ -10,11 +10,12 @@ import android.widget.TextView;
import org.briarproject.R; import org.briarproject.R;
@UiThread @UiThread
public class EmojiSpan extends AnimatingImageSpan { class EmojiSpan extends AnimatingImageSpan {
private final int size; private final int size;
private final FontMetricsInt fm; private final FontMetricsInt fm;
public EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) { EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) {
super(drawable, tv); super(drawable, tv);
fm = tv.getPaint().getFontMetricsInt(); fm = tv.getPaint().getFontMetricsInt();
size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent) size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent)

View File

@@ -7,12 +7,16 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable; import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import static android.text.TextUtils.TruncateAt.END;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.widget.TextView.BufferType.SPANNABLE;
@UiThread @UiThread
public class EmojiTextView extends TextView { public class EmojiTextView extends TextView {
@@ -41,9 +45,7 @@ public class EmojiTextView extends TextView {
} }
private void setTextEllipsized(final @Nullable CharSequence source) { private void setTextEllipsized(final @Nullable CharSequence source) {
super.setText( super.setText(needsEllipsizing ? ellipsize(source) : source, SPANNABLE);
needsEllipsizing ? ellipsize(source) : source,
BufferType.SPANNABLE);
} }
@Override @Override
@@ -56,18 +58,16 @@ public class EmojiTextView extends TextView {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int size = MeasureSpec.getSize(widthMeasureSpec); final int size = MeasureSpec.getSize(widthMeasureSpec);
final int mode = MeasureSpec.getMode(widthMeasureSpec); final int mode = MeasureSpec.getMode(widthMeasureSpec);
if (getEllipsize() == TruncateAt.END && if (getEllipsize() == END &&
!TextUtils.isEmpty(source) && !TextUtils.isEmpty(source) &&
(mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) && (mode == AT_MOST || mode == EXACTLY) &&
getPaint().breakText(source, 0, source.length() - 1, true, size, getPaint().breakText(source, 0, source.length() - 1, true, size,
null) != source.length()) { null) != source.length()) {
needsEllipsizing = true; needsEllipsizing = true;
FontMetricsInt font = getPaint().getFontMetricsInt(); FontMetricsInt font = getPaint().getFontMetricsInt();
super.onMeasure( int height = Math.abs(font.top - font.bottom);
MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY), super.onMeasure(MeasureSpec.makeMeasureSpec(size, EXACTLY),
MeasureSpec MeasureSpec.makeMeasureSpec(height, EXACTLY));
.makeMeasureSpec(Math.abs(font.top - font.bottom),
MeasureSpec.EXACTLY));
} else { } else {
needsEllipsizing = false; needsEllipsizing = false;
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -84,13 +84,11 @@ public class EmojiTextView extends TextView {
@Nullable @Nullable
public CharSequence ellipsize(@Nullable CharSequence text) { public CharSequence ellipsize(@Nullable CharSequence text) {
if (TextUtils.isEmpty(text) || getWidth() == 0 || if (TextUtils.isEmpty(text) || getWidth() == 0 ||
getEllipsize() != TruncateAt.END) { getEllipsize() != END) {
return text; return text;
} else { } else {
return TextUtils.ellipsize(text, return TextUtils.ellipsize(text, getPaint(),
getPaint(), getWidth() - getPaddingRight() - getPaddingLeft(), END);
getWidth() - getPaddingRight() - getPaddingLeft(),
TruncateAt.END);
} }
} }

View File

@@ -13,14 +13,18 @@ import android.view.View;
import org.briarproject.R; import org.briarproject.R;
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.Align.CENTER;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
@UiThread @UiThread
public class EmojiView extends View implements Drawable.Callback { public class EmojiView extends View implements Drawable.Callback {
private final Paint paint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
private String emoji; private String emoji;
private Drawable drawable; private Drawable drawable;
private final Paint paint =
new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
public EmojiView(Context context) { public EmojiView(Context context) {
this(context, null); this(context, null);
} }
@@ -60,7 +64,7 @@ public class EmojiView extends View implements Drawable.Callback {
paint.setTextSize(targetFontSize); paint.setTextSize(targetFontSize);
paint.setColor(ContextCompat paint.setColor(ContextCompat
.getColor(getContext(), R.color.emoji_text_color)); .getColor(getContext(), R.color.emoji_text_color));
paint.setTextAlign(Paint.Align.CENTER); paint.setTextAlign(CENTER);
int xPos = (canvas.getWidth() / 2); int xPos = (canvas.getWidth() / 2);
int yPos = (int) ((canvas.getHeight() / 2) - int yPos = (int) ((canvas.getHeight() / 2) -
((paint.descent() + paint.ascent()) / 2)); ((paint.descent() + paint.ascent()) / 2));
@@ -81,9 +85,4 @@ public class EmojiView extends View implements Drawable.Callback {
super.invalidateDrawable(drawable); super.invalidateDrawable(drawable);
postInvalidate(); postInvalidate();
} }
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
} }

View File

@@ -20,16 +20,14 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE; import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE;
@UiThread @UiThread
public class RecentEmojiPageModel implements EmojiPageModel { public class RecentEmojiPageModel implements EmojiPageModel {
private static final String TAG = private static final Logger LOG =
RecentEmojiPageModel.class.getSimpleName(); Logger.getLogger(RecentEmojiPageModel.class.getName());
private static final Logger LOG = Logger.getLogger(TAG);
private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent"; private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent";
private static final int EMOJI_LRU_SIZE = 50; private static final int EMOJI_LRU_SIZE = 50;
@@ -38,11 +36,11 @@ public class RecentEmojiPageModel implements EmojiPageModel {
private Settings settings; private Settings settings;
@Inject @Inject
protected SettingsManager settingsManager; SettingsManager settingsManager;
@Inject @Inject
protected DbController dbController; DbController dbController;
public RecentEmojiPageModel(Context context) { RecentEmojiPageModel(Context context) {
if (!(context instanceof BaseActivity)) { if (!(context instanceof BaseActivity)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Needs to be created from BaseActivity"); "Needs to be created from BaseActivity");
@@ -85,9 +83,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
return null; return null;
} }
public void onCodePointSelected(String emoji) { void onCodePointSelected(String emoji) {
if (LOG.isLoggable(INFO))
LOG.info("onCodePointSelected(" + emoji + ")");
recentlyUsed.remove(emoji); recentlyUsed.remove(emoji);
recentlyUsed.add(emoji); recentlyUsed.add(emoji);
@@ -105,7 +101,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
result += emoji + ";"; result += emoji + ";";
} }
if (!emojis.isEmpty()) if (!emojis.isEmpty())
result = result.substring(0, result.length()-1); result = result.substring(0, result.length() - 1);
return result; return result;
} }

View File

@@ -8,7 +8,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
@UiThread @UiThread
public class StaticEmojiPageModel implements EmojiPageModel { class StaticEmojiPageModel implements EmojiPageModel {
@DrawableRes @DrawableRes
private final int icon; private final int icon;
@@ -17,14 +17,14 @@ public class StaticEmojiPageModel implements EmojiPageModel {
@Nullable @Nullable
private final String sprite; private final String sprite;
public StaticEmojiPageModel(@DrawableRes int icon, @NonNull String[] emoji, StaticEmojiPageModel(@DrawableRes int icon, @NonNull String[] emoji,
@Nullable String sprite) { @Nullable String sprite) {
this.icon = icon; this.icon = icon;
this.emoji = emoji; this.emoji = emoji;
this.sprite = sprite; this.sprite = sprite;
} }
public StaticEmojiPageModel(Context ctx, @DrawableRes int icon, StaticEmojiPageModel(Context ctx, @DrawableRes int icon,
@ArrayRes int res, @Nullable String sprite) { @ArrayRes int res, @Nullable String sprite) {
this(icon, getEmoji(ctx, res), sprite); this(icon, getEmoji(ctx, res), sprite);
} }
@@ -35,6 +35,7 @@ public class StaticEmojiPageModel implements EmojiPageModel {
return icon; return icon;
} }
@Override
@NonNull @NonNull
public String[] getEmoji() { public String[] getEmoji() {
return emoji; return emoji;

View File

@@ -2,11 +2,11 @@ package org.thoughtcrime.securesms.util;
public class BitmapDecodingException extends Exception { public class BitmapDecodingException extends Exception {
public BitmapDecodingException(String s) { BitmapDecodingException(String s) {
super(s); super(s);
} }
public BitmapDecodingException(Exception nested) { BitmapDecodingException(Exception nested) {
super(nested); super(nested);
} }
} }

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.util;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
@@ -17,10 +16,14 @@ import com.bumptech.glide.load.resource.bitmap.FitCenter;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
public class BitmapUtil { public class BitmapUtil {
private static final String TAG = BitmapUtil.class.getSimpleName(); private static final Logger LOG =
Logger.getLogger(BitmapUtil.class.getName());
private static <T> InputStream getInputStreamForModel(Context context, private static <T> InputStream getInputStreamForModel(Context context,
T model) T model)
@@ -40,8 +43,7 @@ public class BitmapUtil {
final Bitmap rough = Downsampler.AT_LEAST final Bitmap rough = Downsampler.AT_LEAST
.decode(getInputStreamForModel(context, model), .decode(getInputStreamForModel(context, model),
Glide.get(context).getBitmapPool(), Glide.get(context).getBitmapPool(),
width, height, width, height, DecodeFormat.PREFER_RGB_565);
DecodeFormat.PREFER_RGB_565);
final Resource<Bitmap> resource = BitmapResource final Resource<Bitmap> resource = BitmapResource
.obtain(rough, Glide.get(context).getBitmapPool()); .obtain(rough, Glide.get(context).getBitmapPool());
@@ -55,8 +57,7 @@ public class BitmapUtil {
} }
public static <T> Bitmap createScaledBitmap(Context context, T model, public static <T> Bitmap createScaledBitmap(Context context, T model,
float scale) float scale) throws BitmapDecodingException {
throws BitmapDecodingException {
Pair<Integer, Integer> dimens = Pair<Integer, Integer> dimens =
getDimensions(getInputStreamForModel(context, model)); getDimensions(getInputStreamForModel(context, model));
return createScaledBitmapInto(context, model, return createScaledBitmapInto(context, model,
@@ -72,9 +73,8 @@ public class BitmapUtil {
BitmapFactory.decodeStream(fis, null, options); BitmapFactory.decodeStream(fis, null, options);
try { try {
fis.close(); fis.close();
} catch (IOException ioe) { } catch (IOException e) {
Log.w(TAG, if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
"failed to close the InputStream after reading image dimensions");
} }
if (options.outWidth == -1 || options.outHeight == -1) { if (options.outWidth == -1 || options.outHeight == -1) {

View File

@@ -29,6 +29,8 @@ import java.util.List;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.assertTrue;
import static org.briarproject.api.identity.Author.Status.UNKNOWN;
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -77,11 +79,12 @@ public class ForumActivityTest {
private List<ForumEntry> getDummyData() { private List<ForumEntry> getDummyData() {
ForumEntry[] forumEntries = new ForumEntry[6]; ForumEntry[] forumEntries = new ForumEntry[6];
for (int i = 0; i < forumEntries.length; i++) { for (int i = 0; i < forumEntries.length; i++) {
forumEntries[i] = AuthorId authorId = new AuthorId(TestUtils.getRandomId());
new ForumEntry(new MessageId(TestUtils.getRandomId()), byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
AUTHORS[i], LEVELS[i], System.currentTimeMillis(), Author author = new Author(authorId, AUTHORS[i], publicKey);
AUTHORS[i], new AuthorId(TestUtils.getRandomId()), forumEntries[i] = new ForumEntry(
Author.Status.UNKNOWN); new MessageId(TestUtils.getRandomId()), AUTHORS[i],
LEVELS[i], System.currentTimeMillis(), author, UNKNOWN);
} }
return new ArrayList<>(Arrays.asList(forumEntries)); return new ArrayList<>(Arrays.asList(forumEntries));
} }