Disable mass APNG animation on low-memory devices.
This commit is contained in:
parent
acbc17c909
commit
92b586c061
12 changed files with 125 additions and 30 deletions
|
@ -20,7 +20,11 @@ public class ApngBufferCacheDecoder implements ResourceDecoder<ByteBuffer, APNGD
|
|||
|
||||
@Override
|
||||
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
|
||||
if (options.get(ApngOptions.ANIMATE)) {
|
||||
return APNGParser.isAPNG(new ByteBufferReader(source));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
20
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngOptions.java
vendored
Normal file
20
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngOptions.java
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
import com.bumptech.glide.load.Option;
|
||||
|
||||
import org.signal.core.util.Conversions;
|
||||
|
||||
/**
|
||||
* Holds options that can be used to alter how APNGs are decoded in Glide.
|
||||
*/
|
||||
public final class ApngOptions {
|
||||
|
||||
private static final String KEY = "org.signal.skip_apng";
|
||||
|
||||
public static Option<Boolean> ANIMATE = Option.disk(KEY, true, (keyBytes, value, messageDigest) -> {
|
||||
messageDigest.update(keyBytes);
|
||||
messageDigest.update(Conversions.intToByteArray(value ? 1 : 0));
|
||||
});
|
||||
|
||||
private ApngOptions() {}
|
||||
}
|
|
@ -26,7 +26,11 @@ public class ApngStreamCacheDecoder implements ResourceDecoder<InputStream, APNG
|
|||
|
||||
@Override
|
||||
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||
if (options.get(ApngOptions.ANIMATE)) {
|
||||
return APNGParser.isAPNG(new StreamReader(source));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
|
@ -29,12 +30,14 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<StickerRecord> stickers;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
private int stickerSize;
|
||||
|
||||
StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
this.stickers = new ArrayList<>();
|
||||
|
||||
setHasStableIds(true);
|
||||
|
@ -52,7 +55,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StickerKeyboardPageViewHolder viewHolder, int i) {
|
||||
viewHolder.bind(glideRequests, eventListener, stickers.get(i), stickerSize);
|
||||
viewHolder.bind(glideRequests, eventListener, stickers.get(i), stickerSize, allowApngAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -93,7 +96,8 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
public void bind(@NonNull GlideRequests glideRequests,
|
||||
@Nullable EventListener eventListener,
|
||||
@NonNull StickerRecord sticker,
|
||||
@Px int size)
|
||||
@Px int size,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
currentSticker = sticker;
|
||||
|
||||
|
@ -102,6 +106,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
itemView.requestLayout();
|
||||
|
||||
glideRequests.load(new DecryptableUri(sticker.getUri()))
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(image);
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
|||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
/**
|
||||
|
@ -69,7 +70,7 @@ public final class StickerKeyboardPageFragment extends Fragment implements Stick
|
|||
GlideRequests glideRequests = GlideApp.with(this);
|
||||
|
||||
this.list = view.findViewById(R.id.sticker_keyboard_list);
|
||||
this.adapter = new StickerKeyboardPageAdapter(glideRequests, this);
|
||||
this.adapter = new StickerKeyboardPageAdapter(glideRequests, this, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()));
|
||||
this.layoutManager = new GridLayoutManager(requireContext(), 2);
|
||||
this.listTouchListener = new StickerRolloverTouchListener(requireContext(), glideRequests, eventListener, this);
|
||||
this.packId = getArguments().getString(KEY_PACK_ID);
|
||||
|
|
|
@ -19,10 +19,12 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
|
|||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageFragment.EventListener;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardRepository.PackListResult;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -147,7 +149,7 @@ public final class StickerKeyboardProvider implements MediaKeyboardProvider,
|
|||
startingIndex = !result.hasRecents() && result.getPacks().size() > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
presenter.present(this, pagerAdapter, new IconProvider(context, result.getPacks()), null, this, null, startingIndex);
|
||||
presenter.present(this, pagerAdapter, new IconProvider(context, result.getPacks(), DeviceProperties.shouldAllowApngStickerAnimation(context)), null, this, null, startingIndex);
|
||||
|
||||
if (isSoloProvider && result.getPacks().isEmpty()) {
|
||||
context.startActivity(StickerManagementActivity.getIntent(context));
|
||||
|
@ -238,10 +240,12 @@ public final class StickerKeyboardProvider implements MediaKeyboardProvider,
|
|||
|
||||
private final Context context;
|
||||
private final List<StickerPackRecord> packs;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
private IconProvider(@NonNull Context context, List<StickerPackRecord> packs) {
|
||||
private IconProvider(@NonNull Context context, List<StickerPackRecord> packs, boolean allowApngAnimation) {
|
||||
this.context = context;
|
||||
this.packs = packs;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -253,6 +257,7 @@ public final class StickerKeyboardProvider implements MediaKeyboardProvider,
|
|||
Uri uri = packs.get(index - 1).getCover().getUri();
|
||||
|
||||
glideRequests.load(new DecryptableStreamUriLoader.DecryptableUri(uri))
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.into(imageView);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
/**
|
||||
|
@ -93,7 +94,7 @@ public final class StickerManagementActivity extends PassphraseRequiredActivity
|
|||
|
||||
private void initView() {
|
||||
this.list = findViewById(R.id.sticker_management_list);
|
||||
this.adapter = new StickerManagementAdapter(GlideApp.with(this), this);
|
||||
this.adapter = new StickerManagementAdapter(GlideApp.with(this), this, DeviceProperties.shouldAllowApngStickerAnimation(this));
|
||||
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(adapter);
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.adapter.SectionedRecyclerViewAdapter;
|
||||
|
@ -38,6 +39,7 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
private final List<StickerSection> sections = new ArrayList<StickerSection>(3) {{
|
||||
StickerSection yourStickers = new StickerSection(TAG_YOUR_STICKERS,
|
||||
|
@ -55,9 +57,10 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
add(messageStickers);
|
||||
}};
|
||||
|
||||
StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -82,7 +85,7 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
|
||||
@Override
|
||||
public void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull StickerSection section, int localPosition) {
|
||||
section.bindViewHolder(viewHolder, localPosition, glideRequests, eventListener);
|
||||
section.bindViewHolder(viewHolder, localPosition, glideRequests, eventListener, allowApngAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -198,14 +201,15 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
int localPosition,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener)
|
||||
@NonNull EventListener eventListener,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
if (localPosition == 0) {
|
||||
((HeaderViewHolder) viewHolder).bind(titleResId);
|
||||
} else if (records.isEmpty()) {
|
||||
((EmptyViewHolder) viewHolder).bind(emptyResId);
|
||||
} else {
|
||||
((StickerViewHolder) viewHolder).bind(glideRequests, eventListener, records.get(localPosition - 1), localPosition == records.size());
|
||||
((StickerViewHolder) viewHolder).bind(glideRequests, eventListener, records.get(localPosition - 1), localPosition == records.size(), allowApngAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +258,8 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
void bind(@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener,
|
||||
@NonNull StickerPackRecord stickerPack,
|
||||
boolean lastInList)
|
||||
boolean lastInList,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
title.setText(stickerPack.getTitle().or(itemView.getResources().getString(R.string.StickerManagementAdapter_untitled)));
|
||||
author.setText(stickerPack.getAuthor().or(itemView.getResources().getString(R.string.StickerManagementAdapter_unknown)));
|
||||
|
@ -268,6 +273,7 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
|||
|
||||
glideRequests.load(new DecryptableUri(stickerPack.getCover().getUri()))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.into(cover);
|
||||
|
||||
if (stickerPack.isInstalled()) {
|
||||
|
|
|
@ -21,10 +21,12 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManifest.Sticker;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
@ -140,7 +142,7 @@ public final class StickerPackPreviewActivity extends PassphraseRequiredActivity
|
|||
this.shareButton = findViewById(R.id.sticker_install_share_button);
|
||||
this.shareButtonImage = findViewById(R.id.sticker_install_share_button_image);
|
||||
|
||||
this.adapter = new StickerPackPreviewAdapter(GlideApp.with(this), this);
|
||||
this.adapter = new StickerPackPreviewAdapter(GlideApp.with(this), this, DeviceProperties.shouldAllowApngStickerAnimation(this));
|
||||
this.layoutManager = new GridLayoutManager(this, 2);
|
||||
this.touchListener = new StickerRolloverTouchListener(this, GlideApp.with(this), this, this);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
|
@ -192,6 +194,7 @@ public final class StickerPackPreviewActivity extends PassphraseRequiredActivity
|
|||
: new StickerRemoteUri(cover.getPackId(), cover.getPackKey(), cover.getId());
|
||||
GlideApp.with(this).load(model)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.set(ApngOptions.ANIMATE, DeviceProperties.shouldAllowApngStickerAnimation(this))
|
||||
.into(coverImage);
|
||||
} else {
|
||||
coverImage.setImageDrawable(null);
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
|
@ -23,10 +24,12 @@ public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<Sticke
|
|||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<StickerManifest.Sticker> list;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
public StickerPackPreviewAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
public StickerPackPreviewAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
this.list = new ArrayList<>();
|
||||
}
|
||||
|
||||
|
@ -37,7 +40,7 @@ public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<Sticke
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StickerViewHolder stickerViewHolder, int i) {
|
||||
stickerViewHolder.bind(glideRequests, list.get(i), eventListener);
|
||||
stickerViewHolder.bind(glideRequests, list.get(i), eventListener, allowApngAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -68,12 +71,17 @@ public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<Sticke
|
|||
this.image = itemView.findViewById(R.id.sticker_install_item_image);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests, @NonNull StickerManifest.Sticker sticker, @NonNull EventListener eventListener) {
|
||||
void bind(@NonNull GlideRequests glideRequests,
|
||||
@NonNull StickerManifest.Sticker sticker,
|
||||
@NonNull EventListener eventListener,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
currentEmoji = sticker.getEmoji();
|
||||
currentGlideModel = sticker.getUri().isPresent() ? new DecryptableStreamUriLoader.DecryptableUri(sticker.getUri().get())
|
||||
: new StickerRemoteUri(sticker.getPackId(), sticker.getPackKey(), sticker.getId());
|
||||
glideRequests.load(currentGlideModel)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.into(image);
|
||||
|
||||
image.setOnLongClickListener(v -> {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Easy access to various properties of the device, typically to make performance-related decisions.
|
||||
*/
|
||||
public final class DeviceProperties {
|
||||
|
||||
/**
|
||||
* Whether or not we believe the device has the performance capabilities to efficiently render
|
||||
* large numbers of APNGs simultaneously.
|
||||
*/
|
||||
public static boolean shouldAllowApngStickerAnimation(@NonNull Context context) {
|
||||
return !isLowMemoryDevice(context) && getMemoryClass(context) >= FeatureFlags.animatedStickerMinimumMemory();
|
||||
}
|
||||
|
||||
public static boolean isLowMemoryDevice(@NonNull Context context) {
|
||||
ActivityManager activityManager = ServiceUtil.getActivityManager(context);
|
||||
return activityManager.isLowRamDevice();
|
||||
}
|
||||
|
||||
public static int getMemoryClass(@NonNull Context context) {
|
||||
ActivityManager activityManager = ServiceUtil.getActivityManager(context);
|
||||
return activityManager.getMemoryClass();
|
||||
}
|
||||
}
|
|
@ -72,6 +72,7 @@ public final class FeatureFlags {
|
|||
private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff";
|
||||
private static final String OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry";
|
||||
private static final String SHARE_SELECTION_LIMIT = "android.share.limit";
|
||||
private static final String ANIMATED_STICKER_MIN_MEMORY = "android.animatedStickerMinMemory";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -100,7 +101,8 @@ public final class FeatureFlags {
|
|||
AUTOMATIC_SESSION_INTERVAL,
|
||||
DEFAULT_MAX_BACKOFF,
|
||||
OKHTTP_AUTOMATIC_RETRY,
|
||||
SHARE_SELECTION_LIMIT
|
||||
SHARE_SELECTION_LIMIT,
|
||||
ANIMATED_STICKER_MIN_MEMORY
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -139,7 +141,8 @@ public final class FeatureFlags {
|
|||
AUTOMATIC_SESSION_INTERVAL,
|
||||
DEFAULT_MAX_BACKOFF,
|
||||
OKHTTP_AUTOMATIC_RETRY,
|
||||
SHARE_SELECTION_LIMIT
|
||||
SHARE_SELECTION_LIMIT,
|
||||
ANIMATED_STICKER_MIN_MEMORY
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -324,6 +327,11 @@ public final class FeatureFlags {
|
|||
return getBoolean(OKHTTP_AUTOMATIC_RETRY, false);
|
||||
}
|
||||
|
||||
/** The minimum amount of memory required for rendering animated stickers in the keyboard and such */
|
||||
public static int animatedStickerMinimumMemory() {
|
||||
return getInteger(ANIMATED_STICKER_MIN_MEMORY, 193);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
Loading…
Add table
Reference in a new issue