瀏覽代碼

Version 5.1.4

Threema 2 年之前
父節點
當前提交
dae813639c
共有 39 個文件被更改,包括 451 次插入124 次删除
  1. 2 2
      app/build.gradle
  2. 7 1
      app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java
  3. 7 3
      app/src/main/java/ch/threema/app/activities/ballot/BallotChooserActivity.java
  4. 8 4
      app/src/main/java/ch/threema/app/activities/ballot/BallotOverviewActivity.java
  5. 15 2
      app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java
  6. 21 4
      app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java
  7. 13 4
      app/src/main/java/ch/threema/app/adapters/DistributionListAdapter.java
  8. 9 2
      app/src/main/java/ch/threema/app/adapters/GroupCallParticipantsAdapter.kt
  9. 13 4
      app/src/main/java/ch/threema/app/adapters/GroupListAdapter.java
  10. 17 3
      app/src/main/java/ch/threema/app/adapters/MessageListAdapter.java
  11. 17 7
      app/src/main/java/ch/threema/app/adapters/MessageListViewHolder.kt
  12. 8 5
      app/src/main/java/ch/threema/app/adapters/RecentListAdapter.java
  13. 10 4
      app/src/main/java/ch/threema/app/adapters/UserListAdapter.java
  14. 13 3
      app/src/main/java/ch/threema/app/adapters/ballot/BallotOverviewListAdapter.java
  15. 2 1
      app/src/main/java/ch/threema/app/archive/ArchiveActivity.java
  16. 11 6
      app/src/main/java/ch/threema/app/archive/ArchiveAdapter.java
  17. 19 3
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  18. 8 6
      app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java
  19. 3 1
      app/src/main/java/ch/threema/app/fragments/MessageSectionFragment.java
  20. 5 2
      app/src/main/java/ch/threema/app/fragments/UserListFragment.java
  21. 5 1
      app/src/main/java/ch/threema/app/fragments/UserMemberListFragment.java
  22. 5 1
      app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.java
  23. 5 1
      app/src/main/java/ch/threema/app/fragments/WorkUserMemberListFragment.java
  24. 7 0
      app/src/main/java/ch/threema/app/glide/AvatarGlideModule.java
  25. 2 1
      app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.java
  26. 17 3
      app/src/main/java/ch/threema/app/globalsearch/GlobalSearchAdapter.java
  27. 1 1
      app/src/main/java/ch/threema/app/processors/MessageAckProcessor.java
  28. 31 10
      app/src/main/java/ch/threema/app/services/AvatarCacheService.java
  29. 53 10
      app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java
  30. 8 1
      app/src/main/java/ch/threema/app/services/AvatarService.java
  31. 25 2
      app/src/main/java/ch/threema/app/services/ContactServiceImpl.java
  32. 9 2
      app/src/main/java/ch/threema/app/services/DistributionListServiceImpl.java
  33. 16 2
      app/src/main/java/ch/threema/app/services/GroupServiceImpl.java
  34. 32 6
      app/src/main/java/ch/threema/app/ui/AvatarListItemUtil.java
  35. 11 9
      app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java
  36. 1 0
      app/src/main/java/ch/threema/app/utils/ShortcutUtil.java
  37. 2 1
      app/src/main/java/ch/threema/app/voip/activities/GroupCallActivity.kt
  38. 11 4
      domain/src/main/java/ch/threema/domain/onprem/OnPremConfigFetcher.java
  39. 2 2
      domain/src/main/java/ch/threema/domain/protocol/csp/connection/MessageQueue.java

+ 2 - 2
app/build.gradle

@@ -17,7 +17,7 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 
 // version codes
-def app_version = "5.1.3"
+def app_version = "5.1.4"
 def beta_suffix = "" // with leading dash
 
 /**
@@ -96,7 +96,7 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 919
+        versionCode 922
         versionName "${app_version}${beta_suffix}"
         resValue "string", "app_name", "Threema"
         // package name used for sync adapter - needs to match mime types below

+ 7 - 1
app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java

@@ -50,6 +50,7 @@ import androidx.lifecycle.LifecycleOwner;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.bumptech.glide.Glide;
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -441,7 +442,12 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 	}
 
 	private ContactDetailAdapter setupAdapter() {
-		ContactDetailAdapter groupMembershipAdapter = new ContactDetailAdapter(this, this.groupList, contact);
+		ContactDetailAdapter groupMembershipAdapter = new ContactDetailAdapter(
+			this,
+			this.groupList,
+			contact,
+			Glide.with(this)
+		);
 		groupMembershipAdapter.setOnClickListener(new ContactDetailAdapter.OnClickListener() {
 			@Override
 			public void onItemClick(View v, GroupModel groupModel) {

+ 7 - 3
app/src/main/java/ch/threema/app/activities/ballot/BallotChooserActivity.java

@@ -32,6 +32,7 @@ import android.widget.ListView;
 
 import androidx.appcompat.app.ActionBar;
 
+import com.bumptech.glide.Glide;
 import com.google.android.material.appbar.AppBarLayout;
 
 import org.slf4j.Logger;
@@ -182,10 +183,13 @@ public class BallotChooserActivity extends ThreemaToolbarActivity implements Lis
 			});
 
 			if (ballots != null) {
-				this.listAdapter = new BallotOverviewListAdapter(this,
+				this.listAdapter = new BallotOverviewListAdapter(
+					this,
 					ballots,
-						this.ballotService,
-						this.contactService);
+					this.ballotService,
+					this.contactService,
+					Glide.with(this)
+				);
 
 				listView.setAdapter(this.listAdapter);
 			}

+ 8 - 4
app/src/main/java/ch/threema/app/activities/ballot/BallotOverviewActivity.java

@@ -35,6 +35,7 @@ import android.widget.ListView;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.view.ActionMode;
 
+import com.bumptech.glide.Glide;
 import com.google.android.material.appbar.AppBarLayout;
 
 import org.slf4j.Logger;
@@ -261,10 +262,13 @@ public class BallotOverviewActivity extends ThreemaToolbarActivity implements Li
 			});
 
 			if (this.ballots != null) {
-				this.listAdapter = new BallotOverviewListAdapter(this,
-						this.ballots,
-						this.ballotService,
-						this.contactService);
+				this.listAdapter = new BallotOverviewListAdapter(
+					this,
+					this.ballots,
+					this.ballotService,
+					this.contactService,
+					Glide.with(this)
+				);
 
 				listView.setAdapter(this.listAdapter);
 			}

+ 15 - 2
app/src/main/java/ch/threema/app/adapters/ContactDetailAdapter.java

@@ -40,6 +40,7 @@ import androidx.annotation.StringRes;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.bumptech.glide.RequestManager;
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.materialswitch.MaterialSwitch;
 import com.google.android.material.textfield.MaterialAutoCompleteTextView;
@@ -82,6 +83,7 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 	private final ContactModel contactModel;
 	private final List<GroupModel> values;
 	private OnClickListener onClickListener;
+	private final @NonNull RequestManager requestManager;
 
 	public static class ItemHolder extends RecyclerView.ViewHolder {
 		public final View view;
@@ -182,10 +184,16 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 		}
 	}
 
-	public ContactDetailAdapter(Context context, List<GroupModel> values, ContactModel contactModel) {
+	public ContactDetailAdapter(
+		Context context,
+		List<GroupModel>values,
+		ContactModel contactModel,
+		@NonNull RequestManager requestManager
+	) {
 		this.context = context;
 		this.values = values;
 		this.contactModel = contactModel;
+		this.requestManager = requestManager;
 
 		try {
 			ServiceManager serviceManager = ThreemaApplication.requireServiceManager();
@@ -223,7 +231,12 @@ public class ContactDetailAdapter extends RecyclerView.Adapter<RecyclerView.View
 			ItemHolder itemHolder = (ItemHolder) holder;
 			final GroupModel groupModel = getItem(position);
 
-			this.groupService.loadAvatarIntoImage(groupModel, itemHolder.avatarView, AvatarOptions.PRESET_DEFAULT_FALLBACK);
+			this.groupService.loadAvatarIntoImage(
+				groupModel,
+				itemHolder.avatarView,
+				AvatarOptions.PRESET_DEFAULT_FALLBACK,
+				requestManager
+			);
 
 			String groupName = groupModel.getName();
 			if (groupName == null) {

+ 21 - 4
app/src/main/java/ch/threema/app/adapters/ContactListAdapter.java

@@ -34,6 +34,7 @@ import android.widget.TextView;
 import androidx.annotation.NonNull;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
+import com.bumptech.glide.RequestManager;
 import com.google.android.material.card.MaterialCardView;
 import com.google.android.material.imageview.ShapeableImageView;
 import com.google.android.material.shape.ShapeAppearanceModel;
@@ -96,6 +97,7 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 	private Integer[] counts;
 	private final LayoutInflater inflater;
 	private final Collator collator;
+	private final @NonNull RequestManager requestManager;
 
 	public interface AvatarListener {
 		void onAvatarClick(View view, int position);
@@ -103,7 +105,15 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		void onRecentlyAddedClick(ContactModel contactModel);
 	}
 
-	public ContactListAdapter(@NonNull Context context, @NonNull List<ContactModel> values, ContactService contactService, PreferenceService preferenceService, IdListService blackListIdentityService, AvatarListener avatarListener) {
+	public ContactListAdapter(
+		@NonNull Context context,
+		@NonNull List<ContactModel> values,
+		ContactService contactService,
+		PreferenceService preferenceService,
+		IdListService blackListIdentityService,
+		AvatarListener avatarListener,
+		@NonNull RequestManager requestManager
+	) {
 		super(context, R.layout.item_contact_list, (List<Object>) (Object) values);
 
 		this.values = updateRecentlyAdded(values);
@@ -117,6 +127,8 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		this.collator = Collator.getInstance();
 		this.collator.setStrength(Collator.PRIMARY);
 
+		this.requestManager = requestManager;
+
 		setupIndexer();
 	}
 
@@ -371,17 +383,22 @@ public class ContactListAdapter extends FilterableListAdapter implements Section
 		}
 
 		if (viewType == VIEW_TYPE_RECENTLY_ADDED) {
-			contactService.loadAvatarIntoImage(contactModel, holder.shapeableAvatarView,
+			contactService.loadAvatarIntoImage(
+				contactModel,
+				holder.shapeableAvatarView,
 				new AvatarOptions.Builder()
 					.setHighRes(true)
-					.toOptions()
+					.toOptions(),
+				requestManager
 			);
 			holder.viewType = VIEW_TYPE_RECENTLY_ADDED;
 		} else {
 			AvatarListItemUtil.loadAvatar(
 				contactModel,
 				this.contactService,
-				holder);
+				holder,
+				requestManager
+			);
 			holder.avatarView.setContentDescription(
 				ThreemaApplication.getAppContext().getString(R.string.edit_type_content_description,
 					ThreemaApplication.getAppContext().getString(R.string.mime_contact),

+ 13 - 4
app/src/main/java/ch/threema/app/adapters/DistributionListAdapter.java

@@ -29,6 +29,8 @@ import android.widget.Filter;
 import android.widget.ListView;
 import android.widget.TextView;
 
+import com.bumptech.glide.Glide;
+
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -54,7 +56,13 @@ public class DistributionListAdapter extends FilterableListAdapter {
 	private final DistributionListService distributionListService;
 	private final FilterResultsListener filterResultsListener;
 
-	public DistributionListAdapter(Context context, List<DistributionListModel> values, List<Integer> checkedItems, DistributionListService distributionListService, FilterResultsListener filterResultsListener) {
+	public DistributionListAdapter(
+		Context context,
+		List<DistributionListModel> values,
+		List<Integer> checkedItems,
+		DistributionListService distributionListService,
+		FilterResultsListener filterResultsListener
+	) {
 		super(context, R.layout.item_distribution_list, (List<Object>) (Object) values);
 
 		this.context = context;
@@ -116,9 +124,10 @@ public class DistributionListAdapter extends FilterableListAdapter {
 
 		// load avatars asynchronously
 		AvatarListItemUtil.loadAvatar(
-				distributionListModel,
-				this.distributionListService,
-				holder
+			distributionListModel,
+			this.distributionListService,
+			holder,
+			Glide.with(context)
 		);
 
 		((ListView)parent).setItemChecked(position, checkedItems.contains(holder.originalPosition));

+ 9 - 2
app/src/main/java/ch/threema/app/adapters/GroupCallParticipantsAdapter.kt

@@ -37,6 +37,7 @@ import ch.threema.app.voip.groupcall.GroupCallThreadUtil
 import ch.threema.app.voip.groupcall.ParticipantSurfaceViewRenderer
 import ch.threema.app.voip.groupcall.sfu.*
 import ch.threema.base.utils.LoggingUtil
+import com.bumptech.glide.RequestManager
 import kotlinx.coroutines.*
 import org.webrtc.EglBase
 
@@ -45,7 +46,8 @@ private val logger = LoggingUtil.getThreemaLogger("GroupCallParticipantsAdapter"
 @UiThread
 class GroupCallParticipantsAdapter(
 	private val contactService: ContactService,
-	private val gutterPx: Int
+	private val gutterPx: Int,
+	private val requestManager: RequestManager,
 ) : RecyclerView.Adapter<GroupCallParticipantsAdapter.GroupCallParticipantViewHolder>() {
 	private val participants: MutableList<Participant> = mutableListOf()
 
@@ -314,7 +316,12 @@ class GroupCallParticipantsAdapter(
 		holder.name.text = participant.name
 
 		if (participant is NormalParticipant) {
-			contactService.loadAvatarIntoImage(participant.contactModel, holder.avatar, AVATAR_OPTIONS)
+			contactService.loadAvatarIntoImage(
+                participant.contactModel,
+                holder.avatar,
+                AVATAR_OPTIONS,
+                requestManager
+            )
 		} else {
 			logger.warn("Unknown group call participant type bound: {}", participant.type)
 			holder.avatar.setImageResource(R.drawable.ic_person_outline)

+ 13 - 4
app/src/main/java/ch/threema/app/adapters/GroupListAdapter.java

@@ -30,6 +30,8 @@ import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
 
+import com.bumptech.glide.Glide;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -55,7 +57,13 @@ public class GroupListAdapter extends FilterableListAdapter {
 	private final FilterResultsListener filterResultsListener;
 
 
-	public GroupListAdapter(Context context, List<GroupModel> values, List<Integer> checkedItems, GroupService groupService, FilterResultsListener filterResultsListener) {
+	public GroupListAdapter(
+		Context context,
+		List<GroupModel> values,
+		List<Integer> checkedItems,
+		GroupService groupService,
+		FilterResultsListener filterResultsListener
+	) {
 		super(context, R.layout.item_group_list, (List<Object>) (Object) values);
 
 		this.context = context;
@@ -124,9 +132,10 @@ public class GroupListAdapter extends FilterableListAdapter {
 
 		// load avatars asynchronously
 		AvatarListItemUtil.loadAvatar(
-				groupModel,
-				this.groupService,
-				holder
+			groupModel,
+			this.groupService,
+			holder,
+			Glide.with(context)
 		);
 
 		((ListView)parent).setItemChecked(position, checkedItems.contains(holder.originalPosition));

+ 17 - 3
app/src/main/java/ch/threema/app/adapters/MessageListAdapter.java

@@ -31,6 +31,7 @@ import androidx.annotation.Nullable;
 import androidx.core.content.ContextCompat;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.bumptech.glide.RequestManager;
 import com.google.android.material.button.MaterialButton;
 
 import java.util.ArrayList;
@@ -98,6 +99,8 @@ public class MessageListAdapter extends AbstractRecyclerAdapter<ConversationMode
 	private RecyclerView recyclerView;
 	private final Map<ConversationModel, MessageListAdapterItem> messageListAdapterItemsCache;
 
+	private final @NonNull RequestManager requestManager;
+
 
 	public static class FooterViewHolder extends RecyclerView.ViewHolder {
 		FooterViewHolder(View itemView) {
@@ -130,8 +133,9 @@ public class MessageListAdapter extends AbstractRecyclerAdapter<ConversationMode
 		@NonNull GroupCallManager groupCallManager,
 		@Nullable String highlightUid,
 		@NonNull ItemClickListener clickListener,
-		@NonNull Map<ConversationModel, MessageListAdapterItem> messageListAdapterItemCache
-	) {
+		@NonNull Map<ConversationModel, MessageListAdapterItem> messageListAdapterItemCache,
+		@NonNull RequestManager requestManager
+		) {
 		this.context = context;
 		this.inflater = LayoutInflater.from(context);
 		this.conversationService = conversationService;
@@ -169,6 +173,8 @@ public class MessageListAdapter extends AbstractRecyclerAdapter<ConversationMode
 
 		this.groupCallManager = groupCallManager;
 		this.messageListAdapterItemsCache = messageListAdapterItemCache;
+
+		this.requestManager = requestManager;
 	}
 
 	@Override
@@ -212,7 +218,15 @@ public class MessageListAdapter extends AbstractRecyclerAdapter<ConversationMode
 		if (viewType == TYPE_ITEM) {
 			View itemView = inflater.inflate(R.layout.item_message_list, viewGroup, false);
 			itemView.setClickable(true);
-			return new MessageListViewHolder(itemView, context, clickForwarder, groupCallManager, messageListItemParams, messageListItemStrings);
+			return new MessageListViewHolder(
+				itemView,
+				context,
+				clickForwarder,
+				groupCallManager,
+				messageListItemParams,
+				messageListItemStrings,
+				requestManager
+			);
 		}
 		return new FooterViewHolder(inflater.inflate(R.layout.footer_message_section, viewGroup, false));
 	}

+ 17 - 7
app/src/main/java/ch/threema/app/adapters/MessageListViewHolder.kt

@@ -55,17 +55,20 @@ import ch.threema.app.voip.groupcall.GroupCallManager
 import ch.threema.app.voip.groupcall.GroupCallObserver
 import ch.threema.base.utils.LoggingUtil
 import ch.threema.storage.models.ConversationModel.NO_RESOURCE
+import com.bumptech.glide.RequestManager
 import com.google.android.material.button.MaterialButton
 import java.util.Objects
 
 private val logger = LoggingUtil.getThreemaLogger("MessageListViewHolder")
 
-class MessageListViewHolder(itemView: View,
-                            private val context: Context,
-                            private val clickListener: MessageListViewHolderClickListener,
-                            private val groupCallManager: GroupCallManager,
-                            private val params: MessageListItemParams,
-                            private val strings: MessageListItemStrings
+class MessageListViewHolder(
+    itemView: View,
+    private val context: Context,
+    private val clickListener: MessageListViewHolderClickListener,
+    private val groupCallManager: GroupCallManager,
+    private val params: MessageListItemParams,
+    private val strings: MessageListItemStrings,
+    private val requestManager: RequestManager,
 ) : RecyclerView.ViewHolder(itemView), GroupCallObserver {
     interface MessageListViewHolderClickListener {
         fun onItemClick(view: View, position: Int)
@@ -269,7 +272,14 @@ class MessageListViewHolder(itemView: View,
 
         AdapterUtil.styleConversation(fromView, params.groupService, messageListAdapterItem.conversationModel)
 
-        AvatarListItemUtil.loadAvatar(messageListAdapterItem.conversationModel, params.contactService, params.groupService, params.distributionListService, avatarListItemHolder)
+        AvatarListItemUtil.loadAvatar(
+            messageListAdapterItem.conversationModel,
+            params.contactService,
+            params.groupService,
+            params.distributionListService,
+            avatarListItemHolder,
+            requestManager
+        )
 
         updateTypingIndicator(messageListAdapterItem)
 

+ 8 - 5
app/src/main/java/ch/threema/app/adapters/RecentListAdapter.java

@@ -30,6 +30,8 @@ import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
 
+import com.bumptech.glide.Glide;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -160,11 +162,12 @@ public class RecentListAdapter extends FilterableListAdapter {
 
 		// load avatars asynchronously
 		AvatarListItemUtil.loadAvatar(
-				conversationModel,
-				this.contactService,
-				this.groupService,
-				this.distributionListService,
-				holder
+			conversationModel,
+			this.contactService,
+			this.groupService,
+			this.distributionListService,
+			holder,
+			Glide.with(holder.avatarView.getContext())
 		);
 
 		((ListView)parent).setItemChecked(position, checkedItems.contains(holder.originalPosition));

+ 10 - 4
app/src/main/java/ch/threema/app/adapters/UserListAdapter.java

@@ -30,6 +30,8 @@ import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
 
+import com.bumptech.glide.RequestManager;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -71,6 +73,7 @@ public class UserListAdapter extends FilterableListAdapter {
 	private final IdListService blacklistService;
 	private final DeadlineListService hiddenChatsListService;
 	private final FilterResultsListener filterResultsListener;
+	private final @NonNull RequestManager requestManager;
 
 	public UserListAdapter(
 		Context context,
@@ -81,7 +84,8 @@ public class UserListAdapter extends FilterableListAdapter {
 		IdListService blacklistService,
 		DeadlineListService hiddenChatsListService,
 		PreferenceService preferenceService,
-		FilterResultsListener filterResultsListener
+		FilterResultsListener filterResultsListener,
+		@NonNull RequestManager requestManager
 	) {
 		super(context, R.layout.item_user_list, (List<Object>) (Object) values);
 
@@ -90,6 +94,7 @@ public class UserListAdapter extends FilterableListAdapter {
 		this.hiddenChatsListService = hiddenChatsListService;
 		this.blacklistService = blacklistService;
 		this.filterResultsListener = filterResultsListener;
+		this.requestManager = requestManager;
 
 		this.values = new ArrayList<>(values);
 		this.values.addAll(getMissingPreselectedContacts(values, preselectedIdentities));
@@ -184,9 +189,10 @@ public class UserListAdapter extends FilterableListAdapter {
 
 		// load avatars asynchronously
 		AvatarListItemUtil.loadAvatar(
-				contactModel,
-				this.contactService,
-				holder
+			contactModel,
+			this.contactService,
+			holder,
+			requestManager
 		);
 
 		position += ((ListView)parent).getHeaderViewsCount();

+ 13 - 3
app/src/main/java/ch/threema/app/adapters/ballot/BallotOverviewListAdapter.java

@@ -28,11 +28,13 @@ import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.TextView;
 
+import com.bumptech.glide.RequestManager;
 import com.google.android.material.button.MaterialButton;
 
 import java.util.List;
 import java.util.Locale;
 
+import androidx.annotation.NonNull;
 import ch.threema.app.R;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.ballot.BallotService;
@@ -54,14 +56,22 @@ public class BallotOverviewListAdapter extends ArrayAdapter<BallotModel> {
 	private final List<BallotModel> values;
 	private final BallotService ballotService;
 	private final ContactService contactService;
-
-	public BallotOverviewListAdapter(Context context, List<BallotModel> values, BallotService ballotService, ContactService contactService) {
+	private final @NonNull RequestManager requestManager;
+
+	public BallotOverviewListAdapter(
+		Context context,
+		List<BallotModel> values,
+		BallotService ballotService,
+		ContactService contactService,
+		@NonNull RequestManager requestManager
+	) {
 		super(context, R.layout.item_ballot_overview, values);
 
 		this.context = context;
 		this.values = values;
 		this.ballotService = ballotService;
 		this.contactService = contactService;
+		this.requestManager = requestManager;
 	}
 
 	private static class BallotOverviewItemHolder extends AvatarListItemHolder {
@@ -98,7 +108,7 @@ public class BallotOverviewListAdapter extends ArrayAdapter<BallotModel> {
 
 		if(ballotModel != null) {
 			final ContactModel contactModel = this.contactService.getByIdentity(ballotModel.getCreatorIdentity());
-			AvatarListItemUtil.loadAvatar(contactModel, contactService, holder);
+			AvatarListItemUtil.loadAvatar(contactModel, contactService, holder, requestManager);
 
 			if(holder.name != null) {
 				holder.name.setText(ballotModel.getName());

+ 2 - 1
app/src/main/java/ch/threema/app/archive/ArchiveActivity.java

@@ -41,6 +41,7 @@ import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.DefaultItemAnimator;
 import androidx.recyclerview.widget.LinearLayoutManager;
 
+import com.bumptech.glide.Glide;
 import com.google.android.material.appbar.MaterialToolbar;
 
 import org.slf4j.Logger;
@@ -133,7 +134,7 @@ public class ArchiveActivity extends ThreemaToolbarActivity implements GenericAl
 			filterMenu.setVisible(false);
 		}
 
-		archiveAdapter = new ArchiveAdapter(this);
+		archiveAdapter = new ArchiveAdapter(this, Glide.with(this));
 		archiveAdapter.setOnClickItemListener(new ArchiveAdapter.OnClickItemListener() {
 			@Override
 			public void onClick(ConversationModel conversationModel, View view, int position) {

+ 11 - 6
app/src/main/java/ch/threema/app/archive/ArchiveAdapter.java

@@ -30,6 +30,8 @@ import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import com.bumptech.glide.RequestManager;
+
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -71,6 +73,7 @@ public class ArchiveAdapter extends RecyclerView.Adapter<ArchiveAdapter.ArchiveV
 	private DistributionListService distributionListService;
 	private DeadlineListService hiddenChatsListService;
 	private SparseBooleanArray checkedItems = new SparseBooleanArray();
+	private final @NonNull RequestManager requestManager;
 
 	class ArchiveViewHolder extends RecyclerView.ViewHolder {
 
@@ -103,9 +106,10 @@ public class ArchiveAdapter extends RecyclerView.Adapter<ArchiveAdapter.ArchiveV
 	private final LayoutInflater inflater;
 	private List<ConversationModel> conversationModels; // Cached copy of conversationModels
 
-	ArchiveAdapter(Context context) {
+	ArchiveAdapter(Context context, @NonNull RequestManager requestManager) {
 		this.context = context;
 		this.inflater = LayoutInflater.from(context);
+		this.requestManager = requestManager;
 
 		try {
 			ServiceManager serviceManager = ThreemaApplication.getServiceManager();
@@ -217,11 +221,12 @@ public class ArchiveAdapter extends RecyclerView.Adapter<ArchiveAdapter.ArchiveV
 
 			// load avatars asynchronously
 			AvatarListItemUtil.loadAvatar(
-					conversationModel,
-					this.contactService,
-					this.groupService,
-					this.distributionListService,
-					holder.avatarListItemHolder
+				conversationModel,
+				this.contactService,
+				this.groupService,
+				this.distributionListService,
+				holder.avatarListItemHolder,
+				requestManager
 			);
 
 			((CheckableRelativeLayout) holder.itemView).setChecked(checkedItems.get(position));

+ 19 - 3
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -120,6 +120,7 @@ import androidx.transition.Slide;
 import androidx.transition.Transition;
 import androidx.transition.TransitionManager;
 
+import com.bumptech.glide.Glide;
 import com.getkeepsafe.taptargetview.TapTarget;
 import com.getkeepsafe.taptargetview.TapTargetView;
 import com.google.android.material.badge.BadgeDrawable;
@@ -3628,7 +3629,12 @@ public class ComposeMessageFragment extends Fragment implements
 			}
 			actionBarSubtitleTextView.setText(groupService.getMembersString(groupModel));
 			actionBarSubtitleTextView.setVisibility(View.VISIBLE);
-			groupService.loadAvatarIntoImage(groupModel, actionBarAvatarView.getAvatarView(), AvatarOptions.PRESET_DEFAULT_FALLBACK);
+			groupService.loadAvatarIntoImage(
+				groupModel,
+				actionBarAvatarView.getAvatarView(),
+				AvatarOptions.PRESET_DEFAULT_FALLBACK,
+				Glide.with(getActivity())
+			);
 			actionBarAvatarView.setBadgeVisible(false);
 			setAvatarContentDescription(R.string.prefs_group_notifications);
 		} else if (this.isDistributionListChat) {
@@ -3638,7 +3644,12 @@ public class ComposeMessageFragment extends Fragment implements
 				actionBarAvatarView.setVisibility(View.GONE);
 				actionBarTitleTextView.setText(getString(R.string.threema_message_to, ""));
 			} else {
-				distributionListService.loadAvatarIntoImage(distributionListModel, actionBarAvatarView.getAvatarView(), AvatarOptions.PRESET_DEFAULT_AVATAR_NO_CACHE);
+				distributionListService.loadAvatarIntoImage(
+					distributionListModel,
+					actionBarAvatarView.getAvatarView(),
+					AvatarOptions.PRESET_DEFAULT_AVATAR_NO_CACHE,
+					Glide.with(getActivity())
+				);
 			}
 			actionBarAvatarView.setBadgeVisible(false);
 			setAvatarContentDescription(R.string.distribution_list);
@@ -3646,7 +3657,12 @@ public class ComposeMessageFragment extends Fragment implements
 			if (contactModel != null) {
 				this.actionBarSubtitleImageView.setContactModel(contactModel);
 				this.actionBarSubtitleImageView.setVisibility(View.VISIBLE);
-				contactService.loadAvatarIntoImage(contactModel, this.actionBarAvatarView.getAvatarView(), AvatarOptions.PRESET_RESPECT_SETTINGS);
+				contactService.loadAvatarIntoImage(
+					contactModel,
+					this.actionBarAvatarView.getAvatarView(),
+					AvatarOptions.PRESET_RESPECT_SETTINGS,
+					Glide.with(getActivity())
+				);
 				this.actionBarAvatarView.setBadgeVisible(contactService.showBadge(contactModel));
 			}
 			setAvatarContentDescription(R.string.prefs_header_chat);

+ 8 - 6
app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java

@@ -65,6 +65,7 @@ import androidx.work.ExistingWorkPolicy;
 import androidx.work.OneTimeWorkRequest;
 import androidx.work.WorkManager;
 
+import com.bumptech.glide.Glide;
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
 import com.google.android.material.tabs.TabLayout;
@@ -662,12 +663,13 @@ public class ContactsSectionFragment
 
 					if (isAdded() && getContext() != null) {
 						contactListAdapter = new ContactListAdapter(
-								getContext(),
-								contactModels,
-								contactService,
-								serviceManager.getPreferenceService(),
-								serviceManager.getBlackListService(),
-								ContactsSectionFragment.this
+							getContext(),
+							contactModels,
+							contactService,
+							serviceManager.getPreferenceService(),
+							serviceManager.getBlackListService(),
+							ContactsSectionFragment.this,
+							Glide.with(getContext())
 						);
 						listView.setAdapter(contactListAdapter);
 					}

+ 3 - 1
app/src/main/java/ch/threema/app/fragments/MessageSectionFragment.java

@@ -64,6 +64,7 @@ import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.SimpleItemAnimator;
 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
 
+import com.bumptech.glide.Glide;
 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
 import com.google.android.material.snackbar.Snackbar;
 
@@ -1593,7 +1594,8 @@ public class MessageSectionFragment extends MainFragment
 									groupCallManager,
 									highlightUid,
 									MessageSectionFragment.this,
-									messageListAdapterItemCache
+									messageListAdapterItemCache,
+									Glide.with(ThreemaApplication.getAppContext())
 								);
 
 								recyclerView.setAdapter(messageListAdapter);

+ 5 - 2
app/src/main/java/ch/threema/app/fragments/UserListFragment.java

@@ -25,11 +25,13 @@ import android.annotation.SuppressLint;
 import android.content.Intent;
 import android.os.AsyncTask;
 
+import com.bumptech.glide.Glide;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import androidx.annotation.NonNull;
 import ch.threema.app.R;
+import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.AddContactActivity;
 import ch.threema.app.adapters.UserListAdapter;
 import ch.threema.app.collections.Functional;
@@ -103,7 +105,8 @@ public class UserListFragment extends RecipientListFragment {
 					blacklistService,
 					hiddenChatsListService,
 					preferenceService,
-					UserListFragment.this
+					UserListFragment.this,
+					Glide.with(ThreemaApplication.getAppContext())
 				);
 				setListAdapter(adapter);
 				if (listInstanceState != null) {

+ 5 - 1
app/src/main/java/ch/threema/app/fragments/UserMemberListFragment.java

@@ -24,11 +24,14 @@ package ch.threema.app.fragments;
 import android.annotation.SuppressLint;
 import android.os.AsyncTask;
 
+import com.bumptech.glide.Glide;
+
 import java.util.ArrayList;
 import java.util.List;
 
 import androidx.annotation.NonNull;
 import ch.threema.app.R;
+import ch.threema.app.ThreemaApplication;
 import ch.threema.app.adapters.UserListAdapter;
 import ch.threema.app.collections.Functional;
 import ch.threema.app.collections.IPredicateNonNull;
@@ -141,7 +144,8 @@ public class UserMemberListFragment extends MemberListFragment {
 					blacklistService,
 					hiddenChatsListService,
 					preferenceService,
-					UserMemberListFragment.this
+					UserMemberListFragment.this,
+					Glide.with(ThreemaApplication.getAppContext())
 				);
 				setListAdapter(adapter);
 				if (listInstanceState != null) {

+ 5 - 1
app/src/main/java/ch/threema/app/fragments/WorkUserListFragment.java

@@ -33,11 +33,14 @@ import android.widget.ListView;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import com.bumptech.glide.Glide;
+
 import java.util.ArrayList;
 import java.util.List;
 
 import androidx.annotation.NonNull;
 import ch.threema.app.R;
+import ch.threema.app.ThreemaApplication;
 import ch.threema.app.activities.DirectoryActivity;
 import ch.threema.app.adapters.UserListAdapter;
 import ch.threema.app.collections.Functional;
@@ -173,7 +176,8 @@ public class WorkUserListFragment extends RecipientListFragment {
 					blacklistService,
 					hiddenChatsListService,
 					preferenceService,
-					WorkUserListFragment.this
+					WorkUserListFragment.this,
+					Glide.with(ThreemaApplication.getAppContext())
 				);
 				setListAdapter(adapter);
 				if (listInstanceState != null) {

+ 5 - 1
app/src/main/java/ch/threema/app/fragments/WorkUserMemberListFragment.java

@@ -25,12 +25,15 @@ import android.annotation.SuppressLint;
 import android.os.AsyncTask;
 import android.widget.AbsListView;
 
+import com.bumptech.glide.Glide;
+
 import androidx.annotation.NonNull;
 
 import java.util.ArrayList;
 import java.util.List;
 
 import ch.threema.app.R;
+import ch.threema.app.ThreemaApplication;
 import ch.threema.app.adapters.UserListAdapter;
 import ch.threema.app.collections.Functional;
 import ch.threema.app.collections.IPredicateNonNull;
@@ -130,7 +133,8 @@ public class WorkUserMemberListFragment extends MemberListFragment {
 						blacklistService,
 						hiddenChatsListService,
 						preferenceService,
-						WorkUserMemberListFragment.this
+						WorkUserMemberListFragment.this,
+						Glide.with(ThreemaApplication.getAppContext())
 					);
 					setListAdapter(adapter);
 					getListView().setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);

+ 7 - 0
app/src/main/java/ch/threema/app/glide/AvatarGlideModule.java

@@ -27,8 +27,10 @@ import android.graphics.Bitmap;
 import androidx.annotation.NonNull;
 
 import com.bumptech.glide.Glide;
+import com.bumptech.glide.GlideBuilder;
 import com.bumptech.glide.Registry;
 import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.load.engine.executor.GlideExecutor;
 import com.bumptech.glide.module.AppGlideModule;
 
 import ch.threema.app.services.AvatarCacheServiceImpl;
@@ -37,6 +39,11 @@ import ch.threema.storage.models.AbstractMessageModel;
 @GlideModule
 public class AvatarGlideModule extends AppGlideModule {
 
+	@Override
+	public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
+		builder.setSourceExecutor(GlideExecutor.newSourceBuilder().setThreadCount(1).build());
+	}
+
 	@Override
 	public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
 		registry.prepend(AvatarCacheServiceImpl.ContactAvatarConfig.class, Bitmap.class, new ContactAvatarModelLoaderFactory(context));

+ 2 - 1
app/src/main/java/ch/threema/app/globalsearch/GlobalSearchActivity.java

@@ -43,6 +43,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.bumptech.glide.Glide;
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.chip.Chip;
 import com.google.android.material.progressindicator.CircularProgressIndicator;
@@ -196,7 +197,7 @@ public class GlobalSearchActivity extends ThreemaToolbarActivity implements Thre
 		emptyTextView = findViewById(R.id.empty_text);
 		progressBar = findViewById(R.id.progress);
 
-		chatsAdapter = new GlobalSearchAdapter(this);
+		chatsAdapter = new GlobalSearchAdapter(this, Glide.with(this));
 		chatsAdapter.setOnClickItemListener(this::showMessage);
 
 		setupChip(R.id.chats, FILTER_CHATS);

+ 17 - 3
app/src/main/java/ch/threema/app/globalsearch/GlobalSearchAdapter.java

@@ -28,6 +28,8 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 
+import com.bumptech.glide.RequestManager;
+
 import org.slf4j.Logger;
 
 import java.util.List;
@@ -65,6 +67,7 @@ public class GlobalSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
 	private OnClickItemListener onClickItemListener;
 	private String queryString;
 	private List<AbstractMessageModel> messageModels; // Cached copy of AbstractMessageModels
+	private @NonNull RequestManager requestManager;
 
 	private static class ItemHolder extends RecyclerView.ViewHolder {
 		private final TextView titleView;
@@ -86,8 +89,9 @@ public class GlobalSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
 		}
 	}
 
-	GlobalSearchAdapter(Context context) {
+	GlobalSearchAdapter(Context context, @NonNull RequestManager requestManager) {
 		this.context = context;
+		this.requestManager = requestManager;
 
 		try {
 			this.groupService = ThreemaApplication.getServiceManager().getGroupService();
@@ -116,7 +120,12 @@ public class GlobalSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
 			if (current instanceof GroupMessageModel) {
 				final ContactModel contactModel = current.isOutbox() ? this.contactService.getMe() : this.contactService.getByIdentity(current.getIdentity());
 				final GroupModel groupModel = groupService.getById(((GroupMessageModel) current).getGroupId());
-				AvatarListItemUtil.loadAvatar(groupModel, groupService, itemHolder.avatarListItemHolder);
+				AvatarListItemUtil.loadAvatar(
+					groupModel,
+					groupService,
+					itemHolder.avatarListItemHolder,
+					requestManager
+				);
 
 				String groupName = NameUtil.getDisplayName(groupModel, groupService);
 				itemHolder.titleView.setText(
@@ -124,7 +133,12 @@ public class GlobalSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
 				);
 			} else {
 				final ContactModel contactModel = this.contactService.getByIdentity(current.getIdentity());
-				AvatarListItemUtil.loadAvatar( current.isOutbox() ? contactService.getMe() : contactModel, contactService, itemHolder.avatarListItemHolder);
+				AvatarListItemUtil.loadAvatar(
+					current.isOutbox() ? contactService.getMe() : contactModel,
+					contactService,
+					itemHolder.avatarListItemHolder,
+					requestManager
+				);
 
 				String name = NameUtil.getDisplayNameOrNickname(context, current, contactService);
 				itemHolder.titleView.setText(

+ 1 - 1
app/src/main/java/ch/threema/app/processors/MessageAckProcessor.java

@@ -55,7 +55,7 @@ public class MessageAckProcessor implements MessageAckListener {
 	public void processAck(@NonNull QueueMessageId queueMessageId) {
 		final long ackReceivedAt = new Date().getTime();
 		logger.info(
-			"Processing server ack for message ID {} from {}",
+			"Processing server ack for message ID {} to {}",
 			queueMessageId.getMessageId(),
 			queueMessageId.getRecipientId()
 		);

+ 31 - 10
app/src/main/java/ch/threema/app/services/AvatarCacheService.java

@@ -24,6 +24,8 @@ package ch.threema.app.services;
 import android.graphics.Bitmap;
 import android.widget.ImageView;
 
+import com.bumptech.glide.RequestManager;
+
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -38,8 +40,10 @@ import ch.threema.storage.models.GroupModel;
 public interface AvatarCacheService {
 
 	/**
-	 * Get the avatar of the provided contact model in high resolution. If an error happens while loading
-	 * the avatar, the default avatar or null is returned.
+	 * Get the avatar of the provided contact model in high resolution. If an error happens while
+	 * loading the avatar, the default avatar or null is returned. Note: Do not call this method
+	 * with the {@link AvatarOptions.DefaultAvatarPolicy#CUSTOM_AVATAR} for contacts that do not
+	 * have a custom avatar. This may cause glide to misbehave :)
 	 *
 	 * @param contactModel if the contact model is null, the neutral contact avatar is returned
 	 * @param options      the options for loading the avatar
@@ -57,11 +61,18 @@ public interface AvatarCacheService {
 	 * @param options      the options for loading the image
 	 */
 	@AnyThread
-	void loadContactAvatarIntoImage(@NonNull ContactModel contactModel, @NonNull ImageView imageView, @NonNull AvatarOptions options);
+	void loadContactAvatarIntoImage(
+		@NonNull ContactModel contactModel,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	);
 
 	/**
-	 * Get the avatar of the provided group model in high resolution. If an error happens while loading
-	 * the avatar, the default avatar or null is returned.
+	 * Get the avatar of the provided group model in high resolution. If an error happens while
+	 * loading the avatar, the default avatar or null is returned. Note: Do not call this method
+	 * with the {@link AvatarOptions.DefaultAvatarPolicy#CUSTOM_AVATAR} for groups that do not have
+	 * a custom avatar. This may cause glide to misbehave :)
 	 *
 	 * @param groupModel if the group model is null, the neutral group avatar is returned
 	 * @param options    the options for loading the avatar
@@ -74,12 +85,17 @@ public interface AvatarCacheService {
 	/**
 	 * Load the avatar directly into the given image view.
 	 *
-	 * @param groupModel  the group model
-	 * @param imageView   the image view
-	 * @param options     the options for loading the image
+	 * @param groupModel the group model
+	 * @param imageView  the image view
+	 * @param options    the options for loading the image
 	 */
 	@AnyThread
-	void loadGroupAvatarIntoImage(@Nullable GroupModel groupModel, @NonNull ImageView imageView, @NonNull AvatarOptions options);
+	void loadGroupAvatarIntoImage(
+		@Nullable GroupModel groupModel,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	);
 
 	/**
 	 * Get the avatar of the provided model in low resolution. If an error happens while loading the
@@ -102,7 +118,12 @@ public interface AvatarCacheService {
 	 * @param options               the options for loading the image
 	 */
 	@AnyThread
-	void loadDistributionListAvatarIntoImage(@NonNull DistributionListModel distributionListModel, @NonNull ImageView imageView, AvatarOptions options);
+	void loadDistributionListAvatarIntoImage(
+		@NonNull DistributionListModel distributionListModel,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	);
 
 	/**
 	 * Clears the cache. This should be called if many (or all) avatars change, e.g., when changing the default avatar color preference.

+ 53 - 10
app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java

@@ -35,6 +35,7 @@ import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
 
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.RequestManager;
 import com.bumptech.glide.load.engine.DiskCacheStrategy;
 import com.bumptech.glide.load.resource.bitmap.BitmapTransitionOptions;
 import com.bumptech.glide.request.transition.DrawableCrossFadeFactory;
@@ -115,8 +116,13 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 
 	@AnyThread
 	@Override
-	public void loadContactAvatarIntoImage(@NonNull ContactModel model, @NonNull ImageView imageView, @NonNull AvatarOptions options) {
-		loadBitmap(new ContactAvatarConfig(model, options), contactPlaceholder, imageView);
+	public void loadContactAvatarIntoImage(
+		@NonNull ContactModel model,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	) {
+		loadBitmap(new ContactAvatarConfig(model, options), contactPlaceholder, imageView, requestManager);
 	}
 
 	@AnyThread
@@ -129,8 +135,13 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 
 	@AnyThread
 	@Override
-	public void loadGroupAvatarIntoImage(@Nullable GroupModel groupModel, @NonNull ImageView imageView, @NonNull AvatarOptions options) {
-		loadBitmap(new GroupAvatarConfig(groupModel, options), groupPlaceholder, imageView);
+	public void loadGroupAvatarIntoImage(
+		@Nullable GroupModel groupModel,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	) {
+		loadBitmap(new GroupAvatarConfig(groupModel, options), groupPlaceholder, imageView, requestManager);
 	}
 
 	@AnyThread
@@ -143,8 +154,13 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 
 	@AnyThread
 	@Override
-	public void loadDistributionListAvatarIntoImage(@NonNull DistributionListModel model, @NonNull ImageView imageView, @NonNull AvatarOptions options) {
-		loadBitmap(new DistributionListAvatarConfig(model, options), distributionListPlaceholder, imageView);
+	public void loadDistributionListAvatarIntoImage(
+		@NonNull DistributionListModel model,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	) {
+		loadBitmap(new DistributionListAvatarConfig(model, options), distributionListPlaceholder, imageView, requestManager);
 	}
 
 	@AnyThread
@@ -199,15 +215,22 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 			return requestBuilder.submit().get();
 		} catch (ExecutionException | InterruptedException e) {
 			logger.error("Error while getting avatar bitmap for configuration " + config, e);
-			Thread.currentThread().interrupt();
+			if (e instanceof InterruptedException) {
+				Thread.currentThread().interrupt();
+			}
 			return null;
 		}
 	}
 
 	@AnyThread
-	private <M extends ReceiverModel> void loadBitmap(@NonNull AvatarConfig<M> config, @Nullable Drawable placeholder, @NonNull ImageView view) {
+	private <M extends ReceiverModel> void loadBitmap(
+		@NonNull AvatarConfig<M> config,
+		@Nullable Drawable placeholder,
+		@NonNull ImageView view,
+		@NonNull RequestManager requestManager
+	) {
 		try {
-			RequestBuilder<Bitmap> requestBuilder = Glide.with(context).asBitmap().load(config).placeholder(placeholder).transition(BitmapTransitionOptions.withCrossFade(factory)).diskCacheStrategy(DiskCacheStrategy.NONE).signature(new ObjectKey(config.state));
+			RequestBuilder<Bitmap> requestBuilder = requestManager.asBitmap().load(config).placeholder(placeholder).transition(BitmapTransitionOptions.withCrossFade(factory)).diskCacheStrategy(DiskCacheStrategy.NONE).signature(new ObjectKey(config.state));
 			if (config.options.disableCache) {
 				requestBuilder = requestBuilder.skipMemoryCache(true);
 			}
@@ -256,6 +279,8 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 
 		abstract long getAvatarState();
 
+		abstract @NonNull String getModelDebugString();
+
 		/**
 		 * The hash code of this class is based only on the parameters that change the actual result, e.g., the resolution,
 		 * the default options and of course the contact, group, or distribution list. The state does not affect the hashcode
@@ -290,7 +315,7 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 		@NonNull
 		@Override
 		public String toString() {
-			return (model != null ? model.toString() : "null") + " " + options;
+			return "'" + getModelDebugString() + "' " + options;
 		}
 	}
 
@@ -329,6 +354,12 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 			}
 			return newState;
 		}
+
+		@NonNull
+		@Override
+		String getModelDebugString() {
+			return model != null ? model.getIdentity() : "null";
+		}
 	}
 
 	/**
@@ -365,6 +396,12 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 			}
 			return newState;
 		}
+
+		@NonNull
+		@Override
+		String getModelDebugString() {
+			return model != null ? model.getCreatorIdentity() + "/" + model.getApiGroupId() : "null";
+		}
 	}
 
 	/**
@@ -388,6 +425,12 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 		long getAvatarState() {
 			return 1;
 		}
+
+		@NonNull
+		@Override
+		String getModelDebugString() {
+			return model != null ? String.valueOf(model.getId()) : "null";
+		}
 	}
 
 	/**

+ 8 - 1
app/src/main/java/ch/threema/app/services/AvatarService.java

@@ -24,6 +24,8 @@ package ch.threema.app.services;
 import android.graphics.Bitmap;
 import android.widget.ImageView;
 
+import com.bumptech.glide.RequestManager;
+
 import androidx.annotation.AnyThread;
 import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
@@ -105,7 +107,12 @@ public interface AvatarService<M extends ReceiverModel> {
 	 * @param options     the options for loading the image
 	 */
 	@AnyThread
-	void loadAvatarIntoImage(@NonNull M model, @NonNull ImageView imageView, @NonNull AvatarOptions options);
+	void loadAvatarIntoImage(
+		@NonNull M model,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	);
 
 	/**
 	 * Get the default avatar even if a custom avatar is set for the given model.

+ 25 - 2
app/src/main/java/ch/threema/app/services/ContactServiceImpl.java

@@ -40,6 +40,7 @@ import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 import androidx.core.content.ContextCompat;
 
+import com.bumptech.glide.RequestManager;
 import com.neilalexander.jnacl.NaCl;
 
 import org.slf4j.Logger;
@@ -120,6 +121,8 @@ import ch.threema.storage.models.ValidationMessage;
 import ch.threema.storage.models.access.AccessModel;
 import java8.util.function.Consumer;
 
+import static ch.threema.app.glide.AvatarOptions.DefaultAvatarPolicy.CUSTOM_AVATAR;
+
 public class ContactServiceImpl implements ContactService {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("ContactServiceImpl");
 
@@ -909,6 +912,13 @@ public class ContactServiceImpl implements ContactService {
 	@AnyThread
 	@Override
 	public Bitmap getAvatar(@Nullable ContactModel contact, @NonNull AvatarOptions options) {
+		// If the custom avatar is requested without default fallback and there is no avatar for
+		// this contact, we can return null directly. Important: This is necessary to prevent glide
+		// from logging an unnecessary error stack trace.
+		if (options.defaultAvatarPolicy == CUSTOM_AVATAR && !hasAvatarOrContactPhoto(contact)) {
+			return null;
+		}
+
 		Bitmap b = this.avatarCacheService.getContactAvatar(contact, options);
 
 		//check if a business avatar update is necessary
@@ -920,6 +930,14 @@ public class ContactServiceImpl implements ContactService {
 		return b;
 	}
 
+	private boolean hasAvatarOrContactPhoto(@Nullable ContactModel contact) {
+		if (contact == null) {
+			return false;
+		}
+
+		return fileService.hasContactAvatarFile(contact) || fileService.hasContactPhotoFile(contact);
+	}
+
 	@Override
 	public @ColorInt int getAvatarColor(@Nullable ContactModel contact) {
 		if ((this.preferenceService == null || this.preferenceService.isDefaultContactPictureColored()) && contact != null) {
@@ -930,8 +948,13 @@ public class ContactServiceImpl implements ContactService {
 
 	@AnyThread
 	@Override
-	public void loadAvatarIntoImage(@NonNull ContactModel model, @NonNull ImageView imageView, @NonNull AvatarOptions options) {
-		avatarCacheService.loadContactAvatarIntoImage(model, imageView, options);
+	public void loadAvatarIntoImage(
+		@NonNull ContactModel model,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	) {
+		avatarCacheService.loadContactAvatarIntoImage(model, imageView, options, requestManager);
 	}
 
 	@AnyThread

+ 9 - 2
app/src/main/java/ch/threema/app/services/DistributionListServiceImpl.java

@@ -25,6 +25,8 @@ import android.content.Context;
 import android.graphics.Bitmap;
 import android.widget.ImageView;
 
+import com.bumptech.glide.RequestManager;
+
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
@@ -138,8 +140,13 @@ public class DistributionListServiceImpl implements DistributionListService {
 	}
 
 	@Override
-	public void loadAvatarIntoImage(@NonNull DistributionListModel model, @NonNull ImageView imageView, @NonNull AvatarOptions options) {
-		avatarCacheService.loadDistributionListAvatarIntoImage(model, imageView, options);
+	public void loadAvatarIntoImage(
+		@NonNull DistributionListModel model,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	) {
+		avatarCacheService.loadDistributionListAvatarIntoImage(model, imageView, options, requestManager);
 	}
 
 	@Override

+ 16 - 2
app/src/main/java/ch/threema/app/services/GroupServiceImpl.java

@@ -35,6 +35,7 @@ import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.bumptech.glide.RequestManager;
 import com.neilalexander.jnacl.NaCl;
 
 import org.apache.commons.io.IOUtils;
@@ -1469,13 +1470,26 @@ public class GroupServiceImpl implements GroupService {
 	@Nullable
 	@Override
 	public Bitmap getAvatar(@Nullable GroupModel groupModel, @NonNull AvatarOptions options) {
+		// If the custom avatar is requested without default fallback and there is no avatar for
+		// this group, we can return null directly. Important: This is necessary to prevent glide
+		// from logging an unnecessary error stack trace.
+		if (options.defaultAvatarPolicy == AvatarOptions.DefaultAvatarPolicy.CUSTOM_AVATAR
+			&& !fileService.hasGroupAvatarFile(groupModel)) {
+			return null;
+		}
+
 		return avatarCacheService.getGroupAvatar(groupModel, options);
 	}
 
 	@AnyThread
 	@Override
-	public void loadAvatarIntoImage(@NonNull GroupModel groupModel, @NonNull ImageView imageView, @NonNull AvatarOptions options) {
-		avatarCacheService.loadGroupAvatarIntoImage(groupModel, imageView, options);
+	public void loadAvatarIntoImage(
+		@NonNull GroupModel groupModel,
+		@NonNull ImageView imageView,
+		@NonNull AvatarOptions options,
+		@NonNull RequestManager requestManager
+	) {
+		avatarCacheService.loadGroupAvatarIntoImage(groupModel, imageView, options, requestManager);
 	}
 
 	@Override

+ 32 - 6
app/src/main/java/ch/threema/app/ui/AvatarListItemUtil.java

@@ -24,6 +24,9 @@ package ch.threema.app.ui;
 import android.view.View;
 import android.widget.ImageView;
 
+import com.bumptech.glide.RequestManager;
+
+import androidx.annotation.NonNull;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.glide.AvatarOptions;
@@ -46,7 +49,9 @@ public class AvatarListItemUtil {
 		final ContactService contactService,
 		final GroupService groupService,
 		final DistributionListService distributionListService,
-		AvatarListItemHolder holder) {
+		AvatarListItemHolder holder,
+		@NonNull RequestManager requestManager
+	) {
 
 		// load avatars asynchronously
 		ImageView avatarView = holder.avatarView.getAvatarView();
@@ -55,19 +60,34 @@ public class AvatarListItemUtil {
 				ThreemaApplication.getAppContext().getString(R.string.edit_type_content_description,
 					ThreemaApplication.getAppContext().getString(R.string.mime_contact),
 					NameUtil.getDisplayNameOrNickname(conversationModel.getContact(), true)));
-			contactService.loadAvatarIntoImage(conversationModel.getContact(), avatarView, AvatarOptions.PRESET_RESPECT_SETTINGS);
+			contactService.loadAvatarIntoImage(
+				conversationModel.getContact(),
+				avatarView,
+				AvatarOptions.PRESET_RESPECT_SETTINGS,
+				requestManager
+			);
 		} else if (conversationModel.isGroupConversation()) {
 			holder.avatarView.setContentDescription(
 				ThreemaApplication.getAppContext().getString(R.string.edit_type_content_description,
 					ThreemaApplication.getAppContext().getString(R.string.group),
 					NameUtil.getDisplayName(conversationModel.getGroup(), groupService)));
-			groupService.loadAvatarIntoImage(conversationModel.getGroup(), avatarView, AvatarOptions.PRESET_DEFAULT_FALLBACK);
+			groupService.loadAvatarIntoImage(
+				conversationModel.getGroup(),
+				avatarView,
+				AvatarOptions.PRESET_DEFAULT_FALLBACK,
+				requestManager
+			);
 		} else if (conversationModel.isDistributionListConversation()) {
 			holder.avatarView.setContentDescription(
 				ThreemaApplication.getAppContext().getString(R.string.edit_type_content_description,
 					ThreemaApplication.getAppContext().getString(R.string.distribution_list),
 					NameUtil.getDisplayName(conversationModel.getDistributionList(), distributionListService)));
-			distributionListService.loadAvatarIntoImage(conversationModel.getDistributionList(), avatarView, AvatarOptions.PRESET_DEFAULT_AVATAR_NO_CACHE);
+			distributionListService.loadAvatarIntoImage(
+				conversationModel.getDistributionList(),
+				avatarView,
+				AvatarOptions.PRESET_DEFAULT_AVATAR_NO_CACHE,
+				requestManager
+			);
 		}
 
 		// Set work badge
@@ -78,7 +98,8 @@ public class AvatarListItemUtil {
 	public static <M extends ReceiverModel> void loadAvatar(
 		final M model,
 		final AvatarService<M> avatarService,
-		AvatarListItemHolder holder
+		AvatarListItemHolder holder,
+		@NonNull RequestManager requestManager
 	) {
 
 		//do nothing
@@ -101,7 +122,12 @@ public class AvatarListItemUtil {
 			options = AvatarOptions.PRESET_DEFAULT_AVATAR_NO_CACHE;
 		}
 
-		avatarService.loadAvatarIntoImage(model, holder.avatarView.getAvatarView(), options);
+		avatarService.loadAvatarIntoImage(
+			model,
+			holder.avatarView.getAvatarView(),
+			options,
+			requestManager
+		);
 
 		holder.avatarView.setVisibility(View.VISIBLE);
 	}

+ 11 - 9
app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java

@@ -21,10 +21,13 @@
 
 package ch.threema.app.utils;
 
+import static ch.threema.storage.models.ContactModel.DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY;
+
 import android.Manifest;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AuthenticatorDescription;
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
@@ -40,6 +43,13 @@ import android.os.Build;
 import android.provider.ContactsContract;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.WorkerThread;
+import androidx.core.content.ContextCompat;
+import androidx.core.util.Pair;
+
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
 
@@ -52,12 +62,6 @@ import java.util.Map;
 import java.util.TreeMap;
 import java.util.regex.PatternSyntaxException;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresPermission;
-import androidx.annotation.WorkerThread;
-import androidx.core.content.ContextCompat;
-import androidx.core.util.Pair;
 import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.exceptions.FileSystemNotPresentException;
@@ -68,8 +72,6 @@ import ch.threema.base.ThreemaException;
 import ch.threema.base.utils.LoggingUtil;
 import ch.threema.storage.models.ContactModel;
 
-import static ch.threema.storage.models.ContactModel.DEFAULT_ANDROID_CONTACT_AVATAR_EXPIRY;
-
 public class AndroidContactUtil {
 	private static final Logger logger = LoggingUtil.getThreemaLogger("AndroidContactUtil");
 	private UserService userService;
@@ -203,7 +205,7 @@ public class AndroidContactUtil {
 		// contactUri will be null if permission is not granted
 		Uri contactUri = getAndroidContactUri(contactModel);
 		if (contactUri != null) {
-			Bitmap bitmap = AvatarConverterUtil.convert(ThreemaApplication.getAppContext(), contactUri);
+			@SuppressLint("MissingPermission") Bitmap bitmap = AvatarConverterUtil.convert(ThreemaApplication.getAppContext(), contactUri);
 
 			if (bitmap != null) {
 				try {

+ 1 - 0
app/src/main/java/ch/threema/app/utils/ShortcutUtil.java

@@ -399,6 +399,7 @@ public final class ShortcutUtil {
 			preferenceService.setList(KEY_RECENT_UIDS, publishedRecentChatsUids.toArray(new String[0]));
 
 			try {
+				logger.info("Set {} dynamic sharing target shortcuts", numPublishableConversations);
 				ShortcutManagerCompat.setDynamicShortcuts(getContext(), shareTargetShortcuts);
 				logger.info("Published most recent {} conversations as sharing target shortcuts", numPublishableConversations);
 			} catch (Exception e) {

+ 2 - 1
app/src/main/java/ch/threema/app/voip/activities/GroupCallActivity.kt

@@ -76,6 +76,7 @@ import ch.threema.app.voip.util.VoipUtil
 import ch.threema.app.voip.viewmodel.GroupCallViewModel
 import ch.threema.base.utils.LoggingUtil
 import ch.threema.storage.models.GroupModel
+import com.bumptech.glide.Glide
 import kotlinx.coroutines.*
 import java.lang.Runnable
 import java.util.*
@@ -629,7 +630,7 @@ class GroupCallActivity : ThreemaActivity(), GenericAlertDialog.DialogClickListe
 		val gutterPx = resources.getDimensionPixelSize(R.dimen.group_call_participants_item_gutter)
 
 		val contactService = ThreemaApplication.requireServiceManager().contactService
-		participantsAdapter = GroupCallParticipantsAdapter(contactService, gutterPx)
+		participantsAdapter = GroupCallParticipantsAdapter(contactService, gutterPx, Glide.with(this))
 		views.participants.layoutManager = participantsLayoutManager
 		views.participants.adapter = participantsAdapter
 		views.participants.addItemDecoration(VerticalGridLayoutGutterDecoration(gutterPx))

+ 11 - 4
domain/src/main/java/ch/threema/domain/onprem/OnPremConfigFetcher.java

@@ -81,13 +81,20 @@ public class OnPremConfigFetcher {
 		} catch (LicenseExpiredException e) {
 			throw new ThreemaException("OnPrem license has expired");
 		} catch (Exception e) {
+			Date newLastUnauthorized = null;
 			try {
 				if (uc != null && (uc.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED || uc.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN)) {
-					lastUnauthorized = new Date();
-					throw new UnauthorizedFetchException("Cannot fetch OnPrem config (unauthorized; check username/password)");
+					newLastUnauthorized = new Date();
 				}
-			} catch (IOException ignored) {
-				// ignored
+			} catch (IOException | NullPointerException ignored) {
+				// After a cold boot without internet connectivity `uc.getResponseCode()` can throw a NPE
+				// Ignore this exception. A `ThreemaException` wrapping the original exception
+				// will be thrown later on.
+			}
+
+			if (newLastUnauthorized != null) {
+				lastUnauthorized = newLastUnauthorized;
+				throw new UnauthorizedFetchException("Cannot fetch OnPrem config (unauthorized; check username/password)");
 			}
 
 			throw new ThreemaException("Cannot fetch OnPrem config (check connectivity)", e);

+ 2 - 2
domain/src/main/java/ch/threema/domain/protocol/csp/connection/MessageQueue.java

@@ -194,7 +194,7 @@ public class MessageQueue implements MessageAckListener, ConnectionStateListener
 	 */
 	@Override
 	public synchronized void processAck(@NonNull QueueMessageId queueMessageId) {
-		logger.debug("Processing server ack for message ID {} from {}", queueMessageId.getMessageId(), queueMessageId.getRecipientId());
+		logger.debug("Processing server ack for message ID {} to {}", queueMessageId.getMessageId(), queueMessageId.getRecipientId());
 
 		// Find this message in the queue and remove it
 		final Iterator<MessageBox> it = queue.iterator();
@@ -208,7 +208,7 @@ public class MessageQueue implements MessageAckListener, ConnectionStateListener
 			}
 		}
 
-		logger.warn("Message ID {} from {} not found in queue", queueMessageId.getMessageId(), queueMessageId.getRecipientId());
+		logger.warn("Message ID {} to {} not found in queue", queueMessageId.getMessageId(), queueMessageId.getRecipientId());
 	}
 
 	public synchronized int getQueueSize() {