mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 09:22:23 +00:00
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:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
45
src/org/thoughtcrime/securesms/util/ObservingLiveData.java
Normal file
45
src/org/thoughtcrime/securesms/util/ObservingLiveData.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
45
src/org/thoughtcrime/securesms/util/Throttler.java
Normal file
45
src/org/thoughtcrime/securesms/util/Throttler.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user