From c61d54391b3a25943f4c85944e04e935b7490be7 Mon Sep 17 00:00:00 2001 From: jubb Date: Thu, 4 Feb 2021 16:57:24 +1100 Subject: [PATCH 1/2] refactor: performance improvements to ProfilePictureView.kt and recyclers in conversations and home screen --- .../conversation/ConversationAdapter.java | 41 +++++++++- .../conversation/ConversationItem.java | 3 + .../securesms/loki/activities/HomeActivity.kt | 1 + .../securesms/loki/activities/HomeAdapter.kt | 5 ++ .../securesms/loki/views/ConversationView.kt | 9 ++- .../loki/views/ProfilePictureView.kt | 77 ++++++++++++------- 6 files changed, 104 insertions(+), 32 deletions(-) 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 561ef5e4da..597b0a7686 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -23,6 +23,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.RecyclerView; + +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -79,10 +81,11 @@ public class ConversationAdapter implements StickyHeaderDecoration.StickyHeaderAdapter { - private static final int MAX_CACHE_SIZE = 40; + private static final int MAX_CACHE_SIZE = 1000; private static final String TAG = ConversationAdapter.class.getSimpleName(); private final Map> messageRecordCache = Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); + private final SparseArray positionToCacheRef = new SparseArray<>(); private static final int MESSAGE_TYPE_OUTGOING = 0; private static final int MESSAGE_TYPE_INCOMING = 1; @@ -191,6 +194,7 @@ public class ConversationAdapter @Override public void changeCursor(Cursor cursor) { messageRecordCache.clear(); + positionToCacheRef.clear(); super.cleanFastRecords(); super.changeCursor(cursor); } @@ -198,8 +202,39 @@ public class ConversationAdapter @Override protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) { int adapterPosition = viewHolder.getAdapterPosition(); - MessageRecord previousRecord = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getRecordForPositionOrThrow(adapterPosition + 1) : null; - MessageRecord nextRecord = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getRecordForPositionOrThrow(adapterPosition - 1) : null; + + String prevCachedId = positionToCacheRef.get(adapterPosition + 1,null); + String nextCachedId = positionToCacheRef.get(adapterPosition - 1, null); + + MessageRecord previousRecord = null; + if (adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1)) { + if (prevCachedId != null && messageRecordCache.containsKey(prevCachedId)) { + SoftReference prevSoftRecord = messageRecordCache.get(prevCachedId); + MessageRecord prevCachedRecord = prevSoftRecord.get(); + if (prevCachedRecord != null) { + previousRecord = prevCachedRecord; + } else { + previousRecord = getRecordForPositionOrThrow(adapterPosition + 1); + } + } else { + previousRecord = getRecordForPositionOrThrow(adapterPosition + 1); + } + } + + MessageRecord nextRecord = null; + if (adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1)) { + if (nextCachedId != null && messageRecordCache.containsKey(nextCachedId)) { + SoftReference nextSoftRecord = messageRecordCache.get(nextCachedId); + MessageRecord nextCachedRecord = nextSoftRecord.get(); + if (nextCachedRecord != null) { + nextRecord = nextCachedRecord; + } else { + nextRecord = getRecordForPositionOrThrow(adapterPosition - 1); + } + } else { + nextRecord = getRecordForPositionOrThrow(adapterPosition - 1); + } + } viewHolder.getView().bind(messageRecord, Optional.fromNullable(previousRecord), 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 005545c955..d481a1f3fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -337,6 +337,9 @@ public class ConversationItem extends LinearLayout if (recipient != null) { recipient.removeListener(this); } + if (profilePictureView != null) { + profilePictureView.recycle(); + } } public MessageRecord getMessageRecord() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index e23e7bb531..2da2a39ef5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -130,6 +130,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe // Set up recycler view val cursor = DatabaseFactory.getThreadDatabase(this).conversationList val homeAdapter = HomeAdapter(this, cursor) + homeAdapter.setHasStableIds(true) homeAdapter.glide = glide homeAdapter.conversationClickListener = this recyclerView.adapter = homeAdapter diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeAdapter.kt index c64d342e0a..6005b53b8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeAdapter.kt @@ -35,6 +35,11 @@ class HomeAdapter(context: Context, cursor: Cursor) : CursorRecyclerViewAdapter< viewHolder.view.bind(thread, isTyping, glide) } + override fun onItemViewRecycled(holder: ViewHolder?) { + super.onItemViewRecycled(holder) + holder?.view?.recycle() + } + private fun getThread(cursor: Cursor): ThreadRecord? { return threadDatabase.readerFor(cursor).current } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt index a60c073983..321ffed35b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt @@ -40,9 +40,8 @@ class ConversationView : LinearLayout { } private fun setUpViewHierarchy() { - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val contentView = inflater.inflate(R.layout.view_conversation, null) - addView(contentView) + LayoutInflater.from(context) + .inflate(R.layout.view_conversation, this) } // endregion @@ -84,6 +83,10 @@ class ConversationView : LinearLayout { } } + fun recycle() { + profilePictureView.recycle() + } + private fun getUserDisplayName(publicKey: String?): String? { if (TextUtils.isEmpty(publicKey)) return null return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey!!) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt index 517cab6288..8fe86292c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt @@ -29,6 +29,7 @@ class ProfilePictureView : RelativeLayout { var additionalDisplayName: String? = null var isRSSFeed = false var isLarge = false + private val imagesCached = mutableSetOf() // region Lifecycle constructor(context: Context) : super(context) { @@ -104,54 +105,78 @@ class ProfilePictureView : RelativeLayout { fun update() { val publicKey = publicKey ?: return val additionalPublicKey = additionalPublicKey - doubleModeImageViewContainer.visibility = if (additionalPublicKey != null && !isRSSFeed) View.VISIBLE else View.INVISIBLE - singleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && !isLarge) View.VISIBLE else View.INVISIBLE - largeSingleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && isLarge) View.VISIBLE else View.INVISIBLE + doubleModeImageViewContainer.visibility = if (additionalPublicKey != null && !isRSSFeed) { + setProfilePictureIfNeeded( + doubleModeImageView1, + publicKey, + displayName, + R.dimen.small_profile_picture_size) + setProfilePictureIfNeeded( + doubleModeImageView2, + additionalPublicKey, + additionalDisplayName, + R.dimen.small_profile_picture_size) + View.VISIBLE + } else { + glide.clear(doubleModeImageView1) + glide.clear(doubleModeImageView2) + View.INVISIBLE + } + singleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && !isLarge) { + setProfilePictureIfNeeded( + singleModeImageView, + publicKey, + displayName, + R.dimen.medium_profile_picture_size) + View.VISIBLE + } else { + glide.clear(singleModeImageView) + View.INVISIBLE + } + largeSingleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && isLarge) { + setProfilePictureIfNeeded( + largeSingleModeImageView, + publicKey, + displayName, + R.dimen.large_profile_picture_size) + View.VISIBLE + } else { + glide.clear(largeSingleModeImageView) + View.INVISIBLE + } rssImageView.visibility = if (isRSSFeed) View.VISIBLE else View.INVISIBLE - setProfilePictureIfNeeded( - doubleModeImageView1, - publicKey, - displayName, - R.dimen.small_profile_picture_size) - setProfilePictureIfNeeded( - doubleModeImageView2, - additionalPublicKey ?: "", - additionalDisplayName, - R.dimen.small_profile_picture_size) - setProfilePictureIfNeeded( - singleModeImageView, - publicKey, - displayName, - R.dimen.medium_profile_picture_size) - setProfilePictureIfNeeded( - largeSingleModeImageView, - publicKey, - displayName, - R.dimen.large_profile_picture_size) } private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) { - glide.clear(imageView) if (publicKey.isNotEmpty()) { - val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false); + if (imagesCached.contains(publicKey)) return + val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) val signalProfilePicture = recipient.contactPhoto if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" - && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { + && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { + glide.clear(imageView) glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) + imagesCached.add(publicKey) } else { val sizeInPX = resources.getDimensionPixelSize(sizeResId) val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) val hepk = if (recipient.isLocalNumber && masterPublicKey != null) masterPublicKey else publicKey + glide.clear(imageView) glide.load(AvatarPlaceholderGenerator.generate( context, sizeInPX, hepk, displayName )).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) + imagesCached.add(publicKey) } } else { imageView.setImageDrawable(null) } } + + fun recycle() { + imagesCached.clear() + } // endregion } \ No newline at end of file From 8eeb17cbc22be853778565276accab04e1ee0e73 Mon Sep 17 00:00:00 2001 From: Brice Date: Mon, 8 Feb 2021 14:12:21 +1100 Subject: [PATCH 2/2] ban labels added in FR + Invite of settings menu added in translations (#414) --- app/src/main/res/layout/activity_settings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 6 ++++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index d393a1cff5..36c74258d5 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -190,7 +190,7 @@ android:textSize="@dimen/medium_font_size" android:textStyle="bold" android:gravity="center" - android:text="Invite" /> + android:text="@string/activity_settings_invite_button_title" /> Oui Non Supprimer + Bannir Veuillez patienter… Enregistrer Note à mon intention @@ -176,6 +177,7 @@ Le message sélectionné sera irrémédiablement supprimé. Les %1$d messages sélectionnés seront irrémédiablement supprimés + Bannir cet utilisateur? Enregistrer dans la mémoire ? La sauvegarde du média dans l’espace de stockage autorisera d’autres applications à y accéder.\n\nContinuer ? @@ -200,6 +202,8 @@ Textos Suppression Suppression des messages… + Bannir + Bannissement de l’utilisateur… Le message original est introuvable Le message original n’est plus disponible @@ -1110,6 +1114,7 @@ Vous avez reçu un message d’échange de clés pour une version de protocole i Détails du message Copier le texte Supprimer le message + Bannir l’utilisateur Transférer le message Renvoyer le message Répondre au message @@ -1413,6 +1418,7 @@ Vous avez reçu un message d’échange de clés pour une version de protocole i Notifications Chats Appareils reliés + Inviter Phrase de récupération Effacer les données diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 590352b8f4..4f8ad62386 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1471,6 +1471,7 @@ Уведомления Чаты Устройства + Пригласить Секретная фраза Очистить данные diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e831ac6d8e..6737a7a7a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1775,6 +1775,7 @@ Notifications Chats Devices + Invite Recovery Phrase Clear Data