diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index a68f4e922c..1cbd7e47e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.Conversions; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; @@ -171,8 +172,7 @@ public class ConversationAdapter case MESSAGE_TYPE_UPDATE: long start = System.currentTimeMillis(); - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType)); + V itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false); itemView.setOnClickListener(view -> { if (clickListener != null) { @@ -197,7 +197,7 @@ public class ConversationAdapter return new PlaceholderViewHolder(v); case MESSAGE_TYPE_HEADER: case MESSAGE_TYPE_FOOTER: - return new HeaderFooterViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false)); + return new HeaderFooterViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false)); default: throw new IllegalStateException("Cannot create viewholder for type: " + viewType); } @@ -417,7 +417,19 @@ public class ConversationAdapter } } + /** + * Provided a pool, this will initialize it with view counts that make sense. + */ @MainThread + static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) { + pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING, 15); + pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING, 15); + pool.setMaxRecycledViews(MESSAGE_TYPE_PLACEHOLDER, 15); + pool.setMaxRecycledViews(MESSAGE_TYPE_HEADER, 1); + pool.setMaxRecycledViews(MESSAGE_TYPE_FOOTER, 1); + pool.setMaxRecycledViews(MESSAGE_TYPE_UPDATE, 5); + } + private void cleanFastRecords() { synchronized (releasedFastRecords) { Iterator recordIterator = fastRecords.iterator(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index a50af2305b..265c1614ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -36,6 +36,7 @@ import android.view.ViewGroup; import android.view.Window; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; import android.widget.ViewSwitcher; @@ -105,6 +106,7 @@ import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity; +import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.HtmlUtil; @@ -161,6 +163,16 @@ public class ConversationFragment extends Fragment { private MessageRequestViewModel messageRequestViewModel; private ConversationViewModel conversationViewModel; + public static void prepare(@NonNull Context context) { + FrameLayout parent = new FrameLayout(context); + parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); + + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received, parent, 10); + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent, parent, 10); + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5); + CachedInflater.from(context).cacheUntilLimit(R.layout.cursor_adapter_header_footer_view, parent, 2); + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -413,6 +425,7 @@ public class ConversationFragment extends Fragment { ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get()); list.setAdapter(adapter); list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false)); + ConversationAdapter.initializePool(list.getRecycledViewPool()); setLastSeen(conversationViewModel.getLastSeen()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 79e8251da8..dd5b266d13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -236,8 +236,6 @@ public class ConversationItem extends LinearLayout implements BindableConversati bodyText.setOnLongClickListener(passthroughClickListener); bodyText.setOnClickListener(passthroughClickListener); - - bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext())); } @Override @@ -522,6 +520,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati bodyText.setClickable(false); bodyText.setFocusable(false); bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context)); + bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext())); if (messageRecord.isRemoteDelete()) { String deletedMessage = context.getString(R.string.ConversationItem_this_message_was_deleted); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index ad6b0f8d73..0810e0d6ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -90,6 +90,7 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder; import org.thoughtcrime.securesms.components.reminder.ShareReminder; import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder; import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder; +import org.thoughtcrime.securesms.conversation.ConversationFragment; import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener; import org.thoughtcrime.securesms.conversationlist.model.MessageResult; import org.thoughtcrime.securesms.conversationlist.model.SearchResult; @@ -118,6 +119,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.AvatarUtil; +import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -264,6 +266,12 @@ public class ConversationListFragment extends MainFragment implements LoaderMana } } + @Override + public void onStart() { + super.onStart(); + ConversationFragment.prepare(requireContext()); + } + @Override public void onPause() { super.onPause(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CachedInflater.java b/app/src/main/java/org/thoughtcrime/securesms/util/CachedInflater.java new file mode 100644 index 0000000000..d4f3c2973d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CachedInflater.java @@ -0,0 +1,133 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.asynclayoutinflater.view.AsyncLayoutInflater; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logging.Log; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * A class that can be used to pre-cache layouts. Usage flow: + * + * - At some point before you want to use the views, call {@link #cacheUntilLimit(int, ViewGroup, int)}. + * - Later, use {@link #inflate(int, ViewGroup, boolean)}, which will prefer using cached views + * before inflating new ones. + */ +public class CachedInflater { + + private static final String TAG = Log.tag(CachedInflater.class); + + private final Context context; + + /** + * Does *not* work with the application context. + */ + public static CachedInflater from(@NonNull Context context) { + return new CachedInflater(context); + } + + private CachedInflater(@NonNull Context context) { + this.context = context; + } + + /** + * Identical to {@link LayoutInflater#inflate(int, ViewGroup, boolean)}, but will prioritize + * pulling a cached view first. + */ + @MainThread + @SuppressWarnings("unchecked") + public V inflate(@LayoutRes int layoutRes, @Nullable ViewGroup parent, boolean attachToRoot) { + View cached = ViewCache.getInstance().pull(layoutRes); + if (cached != null) { + if (parent != null && attachToRoot) { + parent.addView(cached); + } + return (V) cached; + } else { + return (V) LayoutInflater.from(context).inflate(layoutRes, parent, attachToRoot); + } + } + + /** + * Will inflate as many views as necessary until the cache holds the amount you specify. + */ + @MainThread + public void cacheUntilLimit(@LayoutRes int layoutRes, @Nullable ViewGroup parent, int limit) { + ViewCache.getInstance().cacheUntilLimit(context, layoutRes, parent, limit); + } + + /** + * Clears all cached views. This should be done if, for instance, the theme changes. + */ + @MainThread + public void clear() { + Log.d(TAG, "Clearing view cache."); + ViewCache.getInstance().clear(); + } + + private static class ViewCache { + + private static final ViewCache INSTANCE = new ViewCache(); + + private final Map> cache = new HashMap<>(); + + private long lastClearTime; + + static ViewCache getInstance() { + return INSTANCE; + } + + @MainThread + void cacheUntilLimit(Context context, @LayoutRes int layoutRes, @Nullable ViewGroup parent, int limit) { + AsyncLayoutInflater inflater = new AsyncLayoutInflater(context); + + int existingCount = Util.getOrDefault(cache, layoutRes, Collections.emptyList()).size(); + int inflateCount = Math.max(limit - existingCount, 0); + + for (int i = 0; i < inflateCount; i++) { + final long enqueueTime = System.currentTimeMillis(); + inflater.inflate(layoutRes, parent, (view, resId, p) -> { + Util.assertMainThread(); + if (enqueueTime < lastClearTime) { + Log.d(TAG, "Prefetch is no longer valid. Ignoring."); + return; + } + + List views = cache.get(resId); + + views = views == null ? new LinkedList<>() : views; + views.add(view); + + cache.put(resId, views); + }); + } + } + + @MainThread + @Nullable View pull(@LayoutRes int layoutRes) { + List views = cache.get(layoutRes); + return views != null && !views.isEmpty() ? views.remove(0) + : null; + } + + @MainThread + void clear() { + lastClearTime = System.currentTimeMillis(); + cache.clear(); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java b/app/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java index caca517614..00d89695af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java @@ -31,6 +31,7 @@ public class DynamicTheme { OverridePendingTransition.invoke(activity); activity.startActivity(intent); OverridePendingTransition.invoke(activity); + CachedInflater.from(activity).clear(); } }