diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java index 419e0cfb5..92b38d22f 100644 --- a/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.emoji; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; @@ -31,7 +32,7 @@ import java.io.IOException; import java.util.List; import java.util.concurrent.ExecutionException; -class EmojiProvider { +public class EmojiProvider { private static final String TAG = EmojiProvider.class.getSimpleName(); private static volatile EmojiProvider instance = null; @@ -111,6 +112,19 @@ class EmojiProvider { return getEmojiDrawable(drawInfo, false); } + public boolean isEmoji(CharSequence emoji) { + return emojiTree.getEmoji(emoji, 0, emoji.length()) != null; + } + + public @Nullable Bitmap getEmojiBitmap(CharSequence emoji, boolean background) { + EmojiDrawInfo drawInfo = emojiTree.getEmoji(emoji, 0, emoji.length()); + EmojiDrawable drawable = ((EmojiDrawable) getEmojiDrawable(drawInfo, background)); + if (drawable != null) { + return drawable.getEmojiBitmap(); + } + return null; + } + protected @Nullable Drawable getEmojiDrawable(@Nullable EmojiDrawInfo drawInfo, boolean background) { if (drawInfo == null) { return null; @@ -158,6 +172,26 @@ class EmojiProvider { this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale; } + private Bitmap getEmojiBitmap() { + Bitmap singleEmoji = Bitmap.createBitmap((int) intrinsicWidth/2, (int) intrinsicHeight/2, Bitmap.Config.ARGB_8888); + + final int row = info.getIndex() / EMOJI_PER_ROW; + final int rowIndex = info.getIndex() % EMOJI_PER_ROW; + + Rect desRect = new Rect(0, 0, (int) intrinsicWidth, (int) intrinsicWidth); + Rect srcRect = new Rect((int)(rowIndex * intrinsicWidth), + (int)(row * intrinsicHeight + row * verticalPad)+1, + (int)(((rowIndex + 1) * intrinsicWidth)-1), + (int)((row + 1) * intrinsicHeight + row * verticalPad)-1); + + Canvas canvas = new Canvas(singleEmoji); + canvas.scale(0.5f, 0.5f); + canvas.drawBitmap(bmp, srcRect, desRect, paint); + + return singleEmoji; + } + + @Override public void draw(@NonNull Canvas canvas) { if (bmp == null) { diff --git a/src/org/thoughtcrime/securesms/map/DataCollectionTask.java b/src/org/thoughtcrime/securesms/map/DataCollectionTask.java index b0e1a7281..b0785ef19 100644 --- a/src/org/thoughtcrime/securesms/map/DataCollectionTask.java +++ b/src/org/thoughtcrime/securesms/map/DataCollectionTask.java @@ -7,10 +7,13 @@ import com.b44t.messenger.DcContext; import com.mapbox.geojson.Feature; import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import org.thoughtcrime.securesms.components.emoji.EmojiProvider; import org.thoughtcrime.securesms.map.model.MapSource; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP_NOW; @@ -20,10 +23,10 @@ import static org.thoughtcrime.securesms.map.MapDataManager.TIME_FRAME; * Created by cyberta on 15.04.19. */ -public class DataCollectionTask extends AsyncTask { +public class DataCollectionTask extends AsyncTask> { public interface DataCollectionCallback { - void onDataCollectionFinished(); + void onDataCollectionFinished(Set emojiCodepoints); } private static final String TAG = DataCollectionTask.class.getSimpleName(); @@ -37,6 +40,7 @@ public class DataCollectionTask extends AsyncTask { private final LatLngBounds.Builder boundingBuilder; private final DcContext dcContext; private final DataCollectionCallback callback; + private final EmojiProvider emojiProvider; public DataCollectionTask(DcContext context, int chatId, @@ -45,6 +49,7 @@ public class DataCollectionTask extends AsyncTask { ConcurrentHashMap featureCollections, ConcurrentHashMap lastPositions, LatLngBounds.Builder boundingBuilder, + EmojiProvider emojiProvider, DataCollectionCallback callback) { this.chatId = chatId; this.contactMapSources = contactMapSources; @@ -54,6 +59,7 @@ public class DataCollectionTask extends AsyncTask { this.dcContext = context; this.callback = callback; this.contactIds = contactIds; + this.emojiProvider = emojiProvider; instances.add(this); } @@ -64,29 +70,32 @@ public class DataCollectionTask extends AsyncTask { } @Override - protected Void doInBackground(Void... voids) { + protected Set doInBackground(Void... voids) { Log.d(TAG, "performance test - collect Data start"); + HashSet emojiCodePoints = new HashSet<>(); DataCollector dataCollector = new DataCollector(dcContext, - contactMapSources, - featureCollections, - lastPositions, - boundingBuilder); + contactMapSources, + featureCollections, + lastPositions, + emojiCodePoints, + emojiProvider, + boundingBuilder); for (int contactId : contactIds) { dataCollector.updateSource(chatId, - contactId, - System.currentTimeMillis() - TIME_FRAME, - TIMESTAMP_NOW); + contactId, + System.currentTimeMillis() - TIME_FRAME, + TIMESTAMP_NOW); if (this.isCancelled()) { break; } } - return null; + return emojiCodePoints; } @Override - protected void onPostExecute(Void value) { + protected void onPostExecute(Set strings) { if (!this.isCancelled()) { - callback.onDataCollectionFinished(); + callback.onDataCollectionFinished(strings); } instances.remove(this); Log.d(TAG, "performance test - collect Data finished"); diff --git a/src/org/thoughtcrime/securesms/map/DataCollector.java b/src/org/thoughtcrime/securesms/map/DataCollector.java index 5aac48899..a43ba57b2 100644 --- a/src/org/thoughtcrime/securesms/map/DataCollector.java +++ b/src/org/thoughtcrime/securesms/map/DataCollector.java @@ -11,14 +11,17 @@ import com.mapbox.geojson.Point; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import org.thoughtcrime.securesms.components.emoji.EmojiProvider; import org.thoughtcrime.securesms.map.model.MapSource; import java.util.ArrayList; import java.util.LinkedList; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static org.thoughtcrime.securesms.map.MapDataManager.ACCURACY; import static org.thoughtcrime.securesms.map.MapDataManager.CONTACT_ID; +import static org.thoughtcrime.securesms.map.MapDataManager.IS_EMOJI_CHAR; import static org.thoughtcrime.securesms.map.MapDataManager.IS_POI; import static org.thoughtcrime.securesms.map.MapDataManager.LAST_LOCATION; import static org.thoughtcrime.securesms.map.MapDataManager.LAST_POSITION_ICON; @@ -41,18 +44,24 @@ public class DataCollector { private ConcurrentHashMap contactMapSources; private ConcurrentHashMap> featureCollections; private ConcurrentHashMap lastPositions; + private Set emojiCodePoints; private LatLngBounds.Builder boundingBuilder; + private EmojiProvider emojiProvider; public DataCollector(DcContext dcContext, ConcurrentHashMap contactMapSources, ConcurrentHashMap> featureCollections, ConcurrentHashMap lastPositions, + Set emojiCodePoints, + EmojiProvider emojiProvider, LatLngBounds.Builder boundingBuilder) { this.dcContext = dcContext; this.contactMapSources = contactMapSources; this.featureCollections = featureCollections; this.lastPositions = lastPositions; this.boundingBuilder = boundingBuilder; + this.emojiCodePoints = emojiCodePoints; + this.emojiProvider = emojiProvider; } @@ -85,6 +94,7 @@ public class DataCollector { ""; boolean isPoi = locations.isIndependent(i); int messageId = locations.getMsgId(i); + boolean isEmojiChar = !codepointChar.isEmpty() && emojiProvider.isEmoji(codepointChar); Feature pointFeature = Feature.fromGeometry(point, new JsonObject(), String.valueOf(locations.getLocationId(i))); pointFeature.addBooleanProperty(MARKER_SELECTED, false); @@ -94,9 +104,17 @@ public class DataCollector { pointFeature.addNumberProperty(MESSAGE_ID, messageId); pointFeature.addNumberProperty(ACCURACY, locations.getAccuracy(i)); pointFeature.addStringProperty(MARKER_CHAR, codepointChar); - pointFeature.addStringProperty(MARKER_ICON, isPoi ? - contactMapMetadata.getMarkerPoi() : - contactMapMetadata.getMarkerIcon()); + pointFeature.addBooleanProperty(IS_EMOJI_CHAR, isEmojiChar); + if (isPoi && isEmojiChar) { + //we save emoji bitmaps in mapboxstyle with the codepoint as the key + pointFeature.addStringProperty(MARKER_ICON, codepointChar); + emojiCodePoints.add(codepointChar); + } else if (isPoi) { + pointFeature.addStringProperty(MARKER_ICON, contactMapMetadata.getMarkerPoi()); + } else { + pointFeature.addStringProperty(MARKER_ICON, contactMapMetadata.getMarkerIcon()); + } + pointFeature.addBooleanProperty(IS_POI, isPoi); if (isPoi && codepointChar.length() == 0 && messageId != 0) { //has a long poi label diff --git a/src/org/thoughtcrime/securesms/map/EmojiBitmapGenerationTask.java b/src/org/thoughtcrime/securesms/map/EmojiBitmapGenerationTask.java new file mode 100644 index 000000000..808bd0906 --- /dev/null +++ b/src/org/thoughtcrime/securesms/map/EmojiBitmapGenerationTask.java @@ -0,0 +1,56 @@ +package org.thoughtcrime.securesms.map; + +import android.graphics.Bitmap; +import android.os.AsyncTask; + +import org.thoughtcrime.securesms.components.emoji.EmojiProvider; +import org.thoughtcrime.securesms.util.Pair; + +import java.lang.ref.WeakReference; + +public class EmojiBitmapGenerationTask extends AsyncTask, Void> { + + public interface EmojiBitmapGenerationCallback { + void onEmojiBitmapCreated(String codePoint, Bitmap emoji); + } + WeakReference callbackWeakReference; + WeakReference emojiProviderWeakReference; + + public EmojiBitmapGenerationTask(EmojiBitmapGenerationCallback callback, EmojiProvider emojiProvider) { + callbackWeakReference = new WeakReference<>(callback); + emojiProviderWeakReference = new WeakReference<>(emojiProvider); + } + + @Override + protected Void doInBackground(String... codePoints) { + for (String codePoint : codePoints) { + EmojiProvider emojiProvider = emojiProviderWeakReference.get(); + if (emojiProvider == null) { + return null; + } + Bitmap emoji = emojiProvider.getEmojiBitmap(codePoint, true); + publishProgress(new Pair<>(codePoint, emoji)); + } + + return null; + } + + @Override + protected void onProgressUpdate(Pair... values) { + super.onProgressUpdate(values); + EmojiBitmapGenerationCallback callback = callbackWeakReference.get(); + if (callback != null) { + callback.onEmojiBitmapCreated(values[0].first(), values[0].second()); + } + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + } + + @Override + protected void onCancelled() { + super.onCancelled(); + } +} diff --git a/src/org/thoughtcrime/securesms/map/MapDataManager.java b/src/org/thoughtcrime/securesms/map/MapDataManager.java index 0f4912fe1..ae94e41fa 100644 --- a/src/org/thoughtcrime/securesms/map/MapDataManager.java +++ b/src/org/thoughtcrime/securesms/map/MapDataManager.java @@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.map; import android.content.Context; import android.graphics.Bitmap; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; + import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcEventCenter; import com.mapbox.android.core.permissions.PermissionsManager; @@ -23,28 +24,33 @@ import com.mapbox.mapboxsdk.style.layers.SymbolLayer; import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.emoji.EmojiProvider; import org.thoughtcrime.securesms.connect.ApplicationDcContext; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.map.DataCollectionTask.DataCollectionCallback; +import org.thoughtcrime.securesms.map.EmojiBitmapGenerationTask.EmojiBitmapGenerationCallback; import org.thoughtcrime.securesms.map.GenerateInfoWindowTask.GenerateInfoWindowCallback; import org.thoughtcrime.securesms.map.model.FilterProvider; import org.thoughtcrime.securesms.map.model.MapSource; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static com.b44t.messenger.DcContext.DC_EVENT_LOCATION_CHANGED; import static com.b44t.messenger.DcContext.DC_GCL_ADD_SELF; import static com.mapbox.mapboxsdk.location.modes.RenderMode.COMPASS; import static com.mapbox.mapboxsdk.style.expressions.Expression.all; +import static com.mapbox.mapboxsdk.style.expressions.Expression.eq; import static com.mapbox.mapboxsdk.style.expressions.Expression.get; import static com.mapbox.mapboxsdk.style.expressions.Expression.has; import static com.mapbox.mapboxsdk.style.expressions.Expression.length; import static com.mapbox.mapboxsdk.style.expressions.Expression.literal; -import static com.mapbox.mapboxsdk.style.expressions.Expression.eq; import static com.mapbox.mapboxsdk.style.expressions.Expression.neq; import static com.mapbox.mapboxsdk.style.expressions.Expression.not; import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase; @@ -81,7 +87,10 @@ import static org.thoughtcrime.securesms.util.BitmapUtil.generateColoredBitmap; * Created by cyberta on 07.03.19. */ -public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateInfoWindowCallback, DataCollectionCallback { +public class MapDataManager implements DcEventCenter.DcEventDelegate, + GenerateInfoWindowCallback, + DataCollectionCallback, + EmojiBitmapGenerationCallback { public static final String MARKER_SELECTED = "MARKER_SELECTED"; public static final String LAST_LOCATION = "LAST_LOCATION"; public static final String CONTACT_ID = "CONTACT_ID"; @@ -90,6 +99,7 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn public static final String MESSAGE_ID = "MESSAGE_ID"; public static final String ACCURACY = "ACCURACY"; public static final String MARKER_CHAR = "MARKER_CHAR"; + public static final String IS_EMOJI_CHAR = "IS_EMOJI_CHAR"; public static final String POI_LONG_DESCRIPTION = "POI_LONG_DESCRIPTION"; public static final String MARKER_ICON = "MARKER_ICON"; public static final String IS_POI = "IS_POI"; @@ -109,6 +119,7 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn private ConcurrentHashMap contactMapSources = new ConcurrentHashMap<>(); private ConcurrentHashMap> featureCollections = new ConcurrentHashMap<>(); private ConcurrentHashMap lastPositions = new ConcurrentHashMap<>(); + private Set emojiCodePoints = Collections.newSetFromMap(new ConcurrentHashMap<>()); private FilterProvider filterProvider = new FilterProvider(); private Feature selectedFeature; private int chatId; @@ -119,6 +130,7 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn private boolean isInitial = true; private boolean showTraces = false; private LocationComponent locationComponent; + private EmojiProvider emojiProvider; public interface MapDataState { void onDataInitialized(LatLngBounds bounds); @@ -133,6 +145,7 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn boundingBuilder = new LatLngBounds.Builder(); this.callback = updateCallback; this.locationComponent = locationComponent; + emojiProvider = EmojiProvider.getInstance(context); initInfoWindowLayer(); initLastPositionLayer(); @@ -189,16 +202,21 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn Log.d(TAG, "updateEvent in MapDataManager called. eventId: " + eventId); int contactId = event.getData1Int(); if (contactMapSources.containsKey(contactId)) { + HashSet emojiCodePoints = new HashSet<>(); DataCollector collector = new DataCollector(dcContext, - contactMapSources, - featureCollections, - lastPositions, null); + contactMapSources, + featureCollections, + lastPositions, + emojiCodePoints, + emojiProvider, + null + ); collector.updateSource(chatId, contactId, System.currentTimeMillis() - TIME_FRAME, TIMESTAMP_NOW); - refreshSource(contactId); + handleEmojiCodepoints(emojiCodePoints); } Log.d(TAG, "updateEvent in MapDataManager called. finished: " + eventId); } @@ -260,7 +278,13 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn } @Override - public void onDataCollectionFinished() { + public void onEmojiBitmapCreated(String codePoint, Bitmap emoji) { + mapboxStyle.addImage(codePoint, emoji); + } + + @Override + public void onDataCollectionFinished(Set emojiCodePoints) { + handleEmojiCodepoints(emojiCodePoints); for (MapSource source : contactMapSources.values()) { initContactBasedLayers(source); refreshSource(source.getContactId()); @@ -279,6 +303,16 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn } } + public void handleEmojiCodepoints(Set emojiCodePoints) { + // generate only new emoji bitmaps, so check the diff + emojiCodePoints.removeAll(this.emojiCodePoints); + if (!emojiCodePoints.isEmpty()) { + EmojiBitmapGenerationTask task = new EmojiBitmapGenerationTask(this, emojiProvider); + task.execute(emojiCodePoints.toArray(new String[0])); + this.emojiCodePoints.addAll(emojiCodePoints); + } + } + public void filterRange(long startTimestamp, long endTimestamp) { int[] contactIds = getContactIds(chatId); filterProvider.setRangeFilter(startTimestamp, endTimestamp); @@ -511,7 +545,9 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn contactMapSources, featureCollections, lastPositions, + boundingBuilder, + emojiProvider, this).execute(); } @@ -523,6 +559,7 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn featureCollections, lastPositions, boundingBuilder, + emojiProvider, this).execute(); } diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index d8212bed9..eeef4ec9d 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -410,4 +410,18 @@ public class BitmapUtil { return bitmap; } + public static Bitmap scaleDown(Bitmap realImage, float maxImageSize, + boolean filter) { + float ratio = Math.min( + (float) maxImageSize / realImage.getWidth(), + (float) maxImageSize / realImage.getHeight()); + int width = Math.round((float) ratio * realImage.getWidth()); + int height = Math.round((float) ratio * realImage.getHeight()); + + Bitmap newBitmap = Bitmap.createScaledBitmap(realImage, width, + height, filter); + return newBitmap; + } + + }