Add sticker support.

No sticker packs are available for use yet, but we now have the
latent ability to send and receive.
This commit is contained in:
Greyson Parrelli
2019-04-17 10:21:30 -04:00
parent d5fffb0132
commit 2a644437fb
447 changed files with 8782 additions and 1132 deletions

View File

@@ -9,12 +9,12 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import java.util.Collections;
import java.util.Set;
@@ -37,7 +37,11 @@ public class AttachmentUtil {
Set<String> allowedTypes = getAllowedAutoDownloadTypes(context);
String contentType = attachment.getContentType();
if (attachment.isVoiceNote() || (MediaUtil.isAudio(attachment) && TextUtils.isEmpty(attachment.getFileName())) || MediaUtil.isLongTextType(attachment.getContentType())) {
if (attachment.isVoiceNote() ||
(MediaUtil.isAudio(attachment) && TextUtils.isEmpty(attachment.getFileName())) ||
MediaUtil.isLongTextType(attachment.getContentType()) ||
attachment.isSticker())
{
return true;
} else if (isNonDocumentType(contentType)) {
return allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));

View File

@@ -64,7 +64,19 @@ public class BitmapUtil {
final int maxImageSize)
throws BitmapDecodingException
{
return createScaledBytes(context, model, maxImageWidth, maxImageHeight, maxImageSize, 1, 0);
return createScaledBytes(context, model, maxImageWidth, maxImageHeight, maxImageSize, CompressFormat.JPEG);
}
@WorkerThread
public static <T> ScaleResult createScaledBytes(Context context,
T model,
int maxImageWidth,
int maxImageHeight,
int maxImageSize,
@NonNull CompressFormat format)
throws BitmapDecodingException
{
return createScaledBytes(context, model, maxImageWidth, maxImageHeight, maxImageSize, format, 1, 0);
}
@WorkerThread
@@ -73,6 +85,7 @@ public class BitmapUtil {
final int maxImageWidth,
final int maxImageHeight,
final int maxImageSize,
@NonNull CompressFormat format,
final int sizeAttempt,
int totalAttempts)
throws BitmapDecodingException
@@ -102,7 +115,7 @@ public class BitmapUtil {
do {
totalAttempts++;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
scaledBitmap.compress(CompressFormat.JPEG, quality, baos);
scaledBitmap.compress(format, quality, baos);
bytes = baos.toByteArray();
Log.d(TAG, "iteration with quality " + quality + " size " + bytes.length + " bytes.");
@@ -122,7 +135,7 @@ public class BitmapUtil {
scaledBitmap = null;
Log.i(TAG, "Halving dimensions and retrying.");
return createScaledBytes(context, model, maxImageWidth / 2, maxImageHeight / 2, maxImageSize, sizeAttempt + 1, totalAttempts);
return createScaledBytes(context, model, maxImageWidth / 2, maxImageHeight / 2, maxImageSize, format, sizeAttempt + 1, totalAttempts);
} else {
throw new BitmapDecodingException("Unable to scale image below " + bytes.length + " bytes.");
}
@@ -184,6 +197,17 @@ public class BitmapUtil {
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
}
public static @NonNull CompressFormat getCompressFormatForContentType(@Nullable String contentType) {
if (contentType == null) return CompressFormat.JPEG;
switch (contentType) {
case MediaUtil.IMAGE_JPEG: return CompressFormat.JPEG;
case MediaUtil.IMAGE_PNG: return CompressFormat.PNG;
case MediaUtil.IMAGE_WEBP: return CompressFormat.WEBP;
default: return CompressFormat.JPEG;
}
}
private static BitmapFactory.Options getImageDimensions(InputStream inputStream)
throws BitmapDecodingException
{

View File

@@ -4,7 +4,7 @@ import android.os.Handler;
/**
* A class that will throttle the number of runnables executed to be at most once every specified
* interval.
* interval. However, it could be longer if events are published consistently.
*
* Useful for performing actions in response to rapid user input, such as inputting text, where you
* don't necessarily want to perform an action after <em>every</em> input.

View File

@@ -283,7 +283,7 @@ public class DirectoryHelper {
@NonNull RecipientDatabase recipientDatabase,
@NonNull Set<String> eligibleContactNumbers)
{
return SignalExecutors.IO.submit(() -> {
return SignalExecutors.UNBOUNDED.submit(() -> {
List<ContactTokenDetails> activeTokens = accountManager.getContacts(eligibleContactNumbers);
if (activeTokens != null) {
@@ -329,7 +329,7 @@ public class DirectoryHelper {
@NonNull RecipientDatabase recipientDatabase,
@NonNull Recipient recipient)
{
return SignalExecutors.IO.submit(() -> {
return SignalExecutors.UNBOUNDED.submit(() -> {
boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED;
boolean systemContact = recipient.isSystemContact();
String number = recipient.getAddress().serialize();
@@ -368,7 +368,7 @@ public class DirectoryHelper {
KeyStore iasKeyStore = getIasKeyStore(context);
for (Set<String> batch : batches) {
Future<Set<String>> future = SignalExecutors.IO.submit(() -> {
Future<Set<String>> future = SignalExecutors.UNBOUNDED.submit(() -> {
return new HashSet<>(accountManager.getRegisteredUsers(iasKeyStore, batch, BuildConfig.MRENCLAVE));
});
futures.add(future);

View File

@@ -9,7 +9,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import org.thoughtcrime.securesms.logging.Log;
import android.util.Pair;
import android.webkit.MimeTypeMap;
@@ -17,6 +16,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MmsSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
@@ -40,6 +41,7 @@ public class MediaUtil {
public static final String IMAGE_PNG = "image/png";
public static final String IMAGE_JPEG = "image/jpeg";
public static final String IMAGE_WEBP = "image/webp";
public static final String IMAGE_GIF = "image/gif";
public static final String AUDIO_AAC = "audio/aac";
public static final String AUDIO_UNSPECIFIED = "audio/*";
@@ -50,7 +52,9 @@ public class MediaUtil {
public static Slide getSlideForAttachment(Context context, Attachment attachment) {
Slide slide = null;
if (isGif(attachment.getContentType())) {
if (attachment.isSticker()) {
slide = new StickerSlide(context, attachment);
} else if (isGif(attachment.getContentType())) {
slide = new GifSlide(context, attachment);
} else if (isImageType(attachment.getContentType())) {
slide = new ImageSlide(context, attachment);

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.util;
import android.arch.lifecycle.MutableLiveData;
import android.database.ContentObserver;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.database.ObservableContent;
import java.io.Closeable;
/**
* Implementation of {@link android.arch.lifecycle.LiveData} that will handle closing the contained
* {@link Closeable} when the value changes.
*/
public class ObservingLiveData<E extends ObservableContent> extends MutableLiveData<E> {
private ContentObserver observer;
@Override
public void setValue(E value) {
E previous = getValue();
if (previous != null) {
previous.unregisterContentObserver(observer);
Util.close(previous);
}
value.registerContentObserver(observer);
super.setValue(value);
}
public void close() {
E value = getValue();
if (value != null) {
value.unregisterContentObserver(observer);
Util.close(value);
}
}
public void registerContentObserver(@NonNull ContentObserver observer) {
this.observer = observer;
}
}

View File

@@ -21,11 +21,10 @@ import android.content.Context;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.ArrayRes;
import android.support.annotation.AttrRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.DimenRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.TypedValue;
@@ -61,4 +60,10 @@ public class ResUtil {
typedArray.recycle();
return resourceIds;
}
public static float getFloat(@NonNull Context context, @DimenRes int resId) {
TypedValue value = new TypedValue();
context.getResources().getValue(resId, value, true);
return value.getFloat();
}
}

View File

@@ -12,12 +12,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.logging.Log;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.whispersystems.libsignal.util.Medium;
@@ -180,6 +179,10 @@ public class TextSecurePreferences {
private static final String GIF_GRID_LAYOUT = "pref_gif_grid_layout";
private static final String SEEN_STICKER_INTRO_TOOLTIP = "pref_seen_sticker_intro_tooltip";
private static final String MEDIA_KEYBOARD_MODE = "pref_media_keyboard_mode";
public static boolean isScreenLockEnabled(@NonNull Context context) {
return getBooleanPreference(context, SCREEN_LOCK, false);
}
@@ -1078,6 +1081,23 @@ public class TextSecurePreferences {
setBooleanPreference(context, NEEDS_MESSAGE_PULL, needsMessagePull);
}
public static boolean hasSeenStickerIntroTooltip(Context context) {
return getBooleanPreference(context, SEEN_STICKER_INTRO_TOOLTIP, false);
}
public static void setHasSeenStickerIntroTooltip(Context context, boolean seenStickerTooltip) {
setBooleanPreference(context, SEEN_STICKER_INTRO_TOOLTIP, seenStickerTooltip);
}
public static void setMediaKeyboardMode(Context context, MediaKeyboardMode mode) {
setStringPreference(context, MEDIA_KEYBOARD_MODE, mode.name());
}
public static MediaKeyboardMode getMediaKeyboardMode(Context context) {
String name = getStringPreference(context, MEDIA_KEYBOARD_MODE, MediaKeyboardMode.EMOJI.name());
return MediaKeyboardMode.valueOf(name);
}
public static void setBooleanPreference(Context context, String key, boolean value) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
}
@@ -1126,4 +1146,9 @@ public class TextSecurePreferences {
return defaultValues;
}
}
// NEVER rename these -- they're persisted by name
public enum MediaKeyboardMode {
EMOJI, STICKER
}
}

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.util;
import android.os.Handler;
/**
* A class that will throttle the number of runnables executed to be at most once every specified
* interval.
*
* Useful for performing actions in response to rapid user input where you want to take action on
* the initial input but prevent follow-up spam.
*
* This is different from {@link Debouncer} in that it will run the first runnable immediately
* instead of waiting for input to die down.
*
* See http://rxmarbles.com/#throttle
*/
public class Throttler {
private static final int WHAT = 8675309;
private final Handler handler;
private final long threshold;
/**
* @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every
* {@code threshold} milliseconds.
*/
public Throttler(long threshold) {
this.handler = new Handler();
this.threshold = threshold;
}
public void publish(Runnable runnable) {
if (handler.hasMessages(WHAT)) {
return;
}
runnable.run();
handler.sendMessageDelayed(handler.obtainMessage(WHAT), threshold);
}
public void clear() {
handler.removeCallbacksAndMessages(null);
}
}

View File

@@ -240,4 +240,16 @@ public class ViewUtil {
public static void setPaddingBottom(@NonNull View view, int padding) {
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), padding);
}
public static boolean isPointInsideView(@NonNull View view, float x, float y) {
int[] location = new int[2];
view.getLocationOnScreen(location);
int viewX = location[0];
int viewY = location[1];
return x > viewX && x < viewX + view.getWidth() &&
y > viewY && y < viewY + view.getHeight();
}
}

View File

@@ -12,18 +12,29 @@ import java.util.concurrent.atomic.AtomicInteger;
public class SignalExecutors {
public static final ExecutorService IO = Executors.newCachedThreadPool(new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger();
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r, "signal-io-" + counter.getAndIncrement());
}
});
public static final ExecutorService UNBOUNDED = Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded"));
public static final ExecutorService BOUNDED = Executors.newFixedThreadPool(Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)), new NumberedThreadFactory("signal-bounded"));
public static final ExecutorService SERIAL = Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial"));
public static ExecutorService newCachedSingleThreadExecutor(final String name) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 15, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), r -> new Thread(r, name));
executor.allowCoreThreadTimeOut(true);
return executor;
}
private static class NumberedThreadFactory implements ThreadFactory {
private final String baseName;
private final AtomicInteger counter;
NumberedThreadFactory(@NonNull String baseName) {
this.baseName = baseName;
this.counter = new AtomicInteger();
}
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r, baseName + "-" + counter.getAndIncrement());
}
}
}