Преглед на файлове

Replace Fresco with Coil

Fresco is replaced with Coil everywhere to make it possible to set 'minSdkVersion'
to 23. But Coil is not used directly to avoid splintering the dependency
everywhere in the code. Coil is wrapped by extension functions for 'ImageView'.

Some shared functionality is moved from 'DisplayUtils' into the
'ImageViewExtensions'.

The exisiting initialization of Coil has also be changed. The usage of the self
initialized OKHttp client is removed. If this one is added the
caching of the http client is used by Coil additionally to memory and
disk cache.

Resolves: #2227, #2376

Signed-off-by: Tim Krüger <t@timkrueger.me>
Tim Krüger преди 2 години
родител
ревизия
49da463971
променени са 78 файла, в които са добавени 1124 реда и са изтрити 1372 реда
  1. 1 8
      app/build.gradle
  2. 14 19
      app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
  3. 3 38
      app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt
  4. 3 12
      app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java
  5. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt
  6. 11 34
      app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt
  7. 2 20
      app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java
  8. 20 33
      app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java
  9. 133 152
      app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt
  10. 10 24
      app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java
  11. 4 13
      app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
  12. 13 46
      app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java
  13. 4 25
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
  14. 4 20
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  15. 4 25
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
  16. 3 3
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
  17. 4 26
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt
  18. 2 13
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  19. 2 9
      app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
  20. 2 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
  21. 16 18
      app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt
  22. 10 19
      app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
  23. 1 1
      app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java
  24. 48 49
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  25. 8 48
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  26. 46 73
      app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt
  27. 283 0
      app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt
  28. 1 1
      app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
  29. 7 34
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt
  30. 10 49
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt
  31. 1 1
      app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt
  32. 7 16
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt
  33. 3 13
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt
  34. 2 2
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt
  35. 7 6
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt
  36. 13 44
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt
  37. 2 15
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java
  38. 2 19
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt
  39. 39 171
      app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
  40. 3 2
      app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt
  41. 2 2
      app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
  42. 32 26
      app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
  43. 0 41
      app/src/main/java/com/nextcloud/talk/utils/OkHttpNetworkFetcherWithCache.java
  44. 2 1
      app/src/main/java/com/nextcloud/talk/utils/text/Spans.java
  45. 1 1
      app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java
  46. 0 0
      app/src/main/java/third/parties/daveKoeller/lgpl-2.1.txt
  47. 138 0
      app/src/main/java/third/parties/fresco/BetterImageSpan.kt
  48. 5 0
      app/src/main/res/drawable/shape_oval.xml
  49. 2 6
      app/src/main/res/layout/account_item.xml
  50. 2 1
      app/src/main/res/layout/activity_main.xml
  51. 62 55
      app/src/main/res/layout/call_activity.xml
  52. 2 2
      app/src/main/res/layout/call_item.xml
  53. 20 17
      app/src/main/res/layout/call_notification_activity.xml
  54. 3 3
      app/src/main/res/layout/controller_conversation_info.xml
  55. 2 5
      app/src/main/res/layout/controller_profile.xml
  56. 2 5
      app/src/main/res/layout/controller_settings.xml
  57. 2 6
      app/src/main/res/layout/current_account_item.xml
  58. 2 2
      app/src/main/res/layout/item_custom_incoming_link_preview_message.xml
  59. 2 2
      app/src/main/res/layout/item_custom_incoming_location_message.xml
  60. 2 2
      app/src/main/res/layout/item_custom_incoming_poll_message.xml
  61. 10 12
      app/src/main/res/layout/item_custom_incoming_preview_message.xml
  62. 2 2
      app/src/main/res/layout/item_custom_incoming_text_message.xml
  63. 2 2
      app/src/main/res/layout/item_custom_incoming_voice_message.xml
  64. 9 11
      app/src/main/res/layout/item_custom_outcoming_preview_message.xml
  65. 2 3
      app/src/main/res/layout/poll_result_voter_item.xml
  66. 2 3
      app/src/main/res/layout/reaction_item.xml
  67. 21 22
      app/src/main/res/layout/reference_inside_message.xml
  68. 2 3
      app/src/main/res/layout/rv_item_browser_file.xml
  69. 4 4
      app/src/main/res/layout/rv_item_contact.xml
  70. 2 2
      app/src/main/res/layout/rv_item_contact_shimmer.xml
  71. 7 8
      app/src/main/res/layout/rv_item_conversation_info_participant.xml
  72. 4 3
      app/src/main/res/layout/rv_item_conversation_with_last_message.xml
  73. 1 2
      app/src/main/res/layout/rv_item_load_more.xml
  74. 4 3
      app/src/main/res/layout/rv_item_search_message.xml
  75. 3 5
      app/src/main/res/layout/shared_item_grid.xml
  76. 3 6
      app/src/main/res/layout/shared_item_list.xml
  77. 4 0
      app/src/main/res/values/dimens.xml
  78. 10 0
      app/src/main/res/values/strings.xml

+ 1 - 8
app/build.gradle

@@ -245,15 +245,8 @@ dependencies {
     implementation 'org.apache.commons:commons-lang3:3.12.0'
     implementation 'com.github.wooplr:Spotlight:1.3'
     implementation 'com.google.code.findbugs:jsr305:3.0.2'
-    implementation('com.github.nextcloud-deps:ChatKit:0.3.0-1', {
-        exclude group: 'com.facebook.fresco'
-    })
+    implementation 'com.github.nextcloud-deps:ChatKit:0.3.1'
 
-    implementation 'com.github.nextcloud-deps.fresco:fresco:v111'
-    implementation 'com.github.nextcloud-deps.fresco:animated-webp:v111'
-    implementation 'com.github.nextcloud-deps.fresco:webpsupport:v111'
-    implementation 'com.github.nextcloud-deps.fresco:animated-gif:v111'
-    implementation 'com.github.nextcloud-deps.fresco:imagepipeline-okhttp3:v111'
     implementation 'joda-time:joda-time:2.12.2'
     implementation "io.coil-kt:coil:${coilKtVersion}"
     implementation "io.coil-kt:coil-gif:${coilKtVersion}"

+ 14 - 19
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -149,7 +149,6 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.content.res.AppCompatResources;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.drawable.DrawableCompat;
 import autodagger.AutoInjector;
@@ -540,20 +539,16 @@ public class CallActivity extends CallBaseActivity {
     private void updateAudioOutputButton(WebRtcAudioManager.AudioDevice activeAudioDevice) {
         switch (activeAudioDevice) {
             case BLUETOOTH:
-                binding.audioOutputButton.getHierarchy().setPlaceholderImage(
-                    AppCompatResources.getDrawable(context, R.drawable.ic_baseline_bluetooth_audio_24));
+                binding.audioOutputButton.setImageResource ( R.drawable.ic_baseline_bluetooth_audio_24);
                 break;
             case SPEAKER_PHONE:
-                binding.audioOutputButton.getHierarchy().setPlaceholderImage(
-                    AppCompatResources.getDrawable(context, R.drawable.ic_volume_up_white_24dp));
+                binding.audioOutputButton.setImageResource(R.drawable.ic_volume_up_white_24dp);
                 break;
             case EARPIECE:
-                binding.audioOutputButton.getHierarchy().setPlaceholderImage(
-                    AppCompatResources.getDrawable(context, R.drawable.ic_baseline_phone_in_talk_24));
+                binding.audioOutputButton.setImageResource(R.drawable.ic_baseline_phone_in_talk_24);
                 break;
             case WIRED_HEADSET:
-                binding.audioOutputButton.getHierarchy().setPlaceholderImage(
-                    AppCompatResources.getDrawable(context, R.drawable.ic_baseline_headset_mic_24));
+                binding.audioOutputButton.setImageResource(R.drawable.ic_baseline_headset_mic_24);
                 break;
             default:
                 Log.e(TAG, "Icon for audio output not available");
@@ -795,7 +790,7 @@ public class CallActivity extends CallBaseActivity {
                     onCameraClick();
                 }
             } else {
-                binding.cameraButton.getHierarchy().setPlaceholderImage(R.drawable.ic_videocam_off_white_24px);
+                binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px);
                 binding.cameraButton.setAlpha(0.7f);
                 binding.switchSelfVideoButton.setVisibility(View.GONE);
             }
@@ -806,7 +801,7 @@ public class CallActivity extends CallBaseActivity {
                 onMicrophoneClick();
             }
         } else {
-            binding.microphoneButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_off_white_24px);
+            binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
         }
 
         if (!isConnectionEstablished()) {
@@ -917,7 +912,7 @@ public class CallActivity extends CallBaseActivity {
 
         if (!canPublishAudioStream) {
             microphoneOn = false;
-            binding.microphoneButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_off_white_24px);
+            binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
             toggleMedia(false, false);
         }
 
@@ -961,12 +956,12 @@ public class CallActivity extends CallBaseActivity {
                 microphoneOn = !microphoneOn;
 
                 if (microphoneOn) {
-                    binding.microphoneButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_white_24px);
+                    binding.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px);
                     updatePictureInPictureActions(R.drawable.ic_mic_white_24px,
                                                   getResources().getString(R.string.nc_pip_microphone_mute),
                                                   MICROPHONE_PIP_REQUEST_MUTE);
                 } else {
-                    binding.microphoneButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_off_white_24px);
+                    binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
                     updatePictureInPictureActions(R.drawable.ic_mic_off_white_24px,
                                                   getResources().getString(R.string.nc_pip_microphone_unmute),
                                                   MICROPHONE_PIP_REQUEST_UNMUTE);
@@ -974,7 +969,7 @@ public class CallActivity extends CallBaseActivity {
 
                 toggleMedia(microphoneOn, false);
             } else {
-                binding.microphoneButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_white_24px);
+                binding.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px);
                 pulseAnimation.start();
                 toggleMedia(true, false);
             }
@@ -997,7 +992,7 @@ public class CallActivity extends CallBaseActivity {
 
         if (!canPublishVideoStream) {
             videoOn = false;
-            binding.cameraButton.getHierarchy().setPlaceholderImage(R.drawable.ic_videocam_off_white_24px);
+            binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px);
             binding.switchSelfVideoButton.setVisibility(View.GONE);
             return;
         }
@@ -1006,12 +1001,12 @@ public class CallActivity extends CallBaseActivity {
             videoOn = !videoOn;
 
             if (videoOn) {
-                binding.cameraButton.getHierarchy().setPlaceholderImage(R.drawable.ic_videocam_white_24px);
+                binding.cameraButton.setImageResource(R.drawable.ic_videocam_white_24px);
                 if (cameraEnumerator.getDeviceNames().length > 1) {
                     binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
                 }
             } else {
-                binding.cameraButton.getHierarchy().setPlaceholderImage(R.drawable.ic_videocam_off_white_24px);
+                binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px);
                 binding.switchSelfVideoButton.setVisibility(View.GONE);
             }
 
@@ -2675,7 +2670,7 @@ public class CallActivity extends CallBaseActivity {
             v.onTouchEvent(event);
             if (event.getAction() == MotionEvent.ACTION_UP && isPushToTalkActive) {
                 isPushToTalkActive = false;
-                binding.microphoneButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_off_white_24px);
+                binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
                 pulseAnimation.stop();
                 toggleMedia(false, false);
                 animateCallControls(false, 5000);

+ 3 - 38
app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt

@@ -28,8 +28,6 @@ import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import android.content.res.Configuration
-import android.graphics.Bitmap
-import android.graphics.drawable.BitmapDrawable
 import android.media.AudioAttributes
 import android.media.MediaPlayer
 import android.os.Build
@@ -41,24 +39,18 @@ import android.view.View
 import androidx.annotation.RequiresApi
 import androidx.core.app.NotificationCompat
 import autodagger.AutoInjector
-import com.facebook.common.executors.UiThreadImmediateExecutorService
-import com.facebook.common.references.CloseableReference
-import com.facebook.datasource.DataSource
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
-import com.facebook.imagepipeline.image.CloseableImage
 import com.nextcloud.talk.R
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.CallNotificationActivityBinding
+import com.nextcloud.talk.extensions.loadAvatar
 import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.participants.Participant
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.DoNotDisturbUtils.shouldPlaySound
 import com.nextcloud.talk.utils.NotificationUtils
 import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
@@ -75,6 +67,7 @@ import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
+import kotlinx.android.synthetic.main.call_item.*
 import okhttp3.Cache
 import org.parceler.Parcels
 import java.io.IOException
@@ -356,7 +349,7 @@ class CallNotificationActivity : CallBaseActivity() {
     private fun setUpAfterConversationIsKnown() {
         binding!!.conversationNameTextView.text = currentConversation!!.displayName
         if (currentConversation!!.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
-            setAvatarForOneToOneCall()
+            avatarImageView.loadAvatar(userBeingCalled!!, currentConversation!!.name!!)
         } else {
             binding!!.avatarImageView.setImageResource(R.drawable.ic_circular_group)
         }
@@ -364,34 +357,6 @@ class CallNotificationActivity : CallBaseActivity() {
         showAnswerControls()
     }
 
-    @Suppress("MagicNumber")
-    private fun setAvatarForOneToOneCall() {
-        val imageRequest = DisplayUtils.getImageRequestForUrl(
-            ApiUtils.getUrlForAvatar(
-                userBeingCalled!!.baseUrl,
-                currentConversation!!.name,
-                true
-            )
-        )
-        val imagePipeline = Fresco.getImagePipeline()
-        val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
-        dataSource.subscribe(
-            object : BaseBitmapDataSubscriber() {
-                override fun onNewResultImpl(bitmap: Bitmap?) {
-                    binding!!.avatarImageView.hierarchy.setImage(
-                        BitmapDrawable(resources, bitmap), 100f,
-                        true
-                    )
-                }
-
-                override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>>) {
-                    Log.e(TAG, "failed to load avatar")
-                }
-            },
-            UiThreadImmediateExecutorService.getInstance()
-        )
-    }
-
     private fun endMediaNotifications() {
         if (mediaPlayer != null) {
             if (mediaPlayer!!.isPlaying) {

+ 3 - 12
app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java

@@ -12,12 +12,9 @@ import android.widget.ProgressBar;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
-import com.facebook.drawee.view.SimpleDraweeView;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.activities.CallActivity;
-import com.nextcloud.talk.utils.DisplayUtils;
+import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
 
 import org.webrtc.MediaStream;
 import org.webrtc.MediaStreamTrack;
@@ -109,7 +106,7 @@ public class ParticipantsAdapter extends BaseAdapter {
         convertView.setLayoutParams(layoutParams);
 
         TextView nickTextView = convertView.findViewById(R.id.peer_nick_text_view);
-        SimpleDraweeView imageView = convertView.findViewById(R.id.avatarImageView);
+        ImageView imageView = convertView.findViewById(R.id.avatarImageView);
 
         MediaStream mediaStream = participantDisplayItem.getMediaStream();
         if (hasVideoStream(participantDisplayItem, mediaStream)) {
@@ -128,13 +125,7 @@ public class ParticipantsAdapter extends BaseAdapter {
                 nickTextView.setVisibility(View.VISIBLE);
                 nickTextView.setText(participantDisplayItem.getNick());
             }
-
-            imageView.setController(null);
-            DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(imageView.getController())
-                    .setImageRequest(DisplayUtils.getImageRequestForUrl(participantDisplayItem.getUrlForAvatar()))
-                    .build();
-            imageView.setController(draweeController);
+            ImageViewExtensionsKt.loadAvatarWithUrl(imageView,null, participantDisplayItem.getUrlForAvatar());
         }
 
         ImageView audioOffView = convertView.findViewById(R.id.remote_audio_off);

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt

@@ -34,7 +34,7 @@ class ReactionsAdapter(
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReactionsViewHolder {
         val itemBinding = ReactionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
-        return ReactionsViewHolder(itemBinding, user?.baseUrl)
+        return ReactionsViewHolder(itemBinding, user)
     }
 
     override fun onBindViewHolder(holder: ReactionsViewHolder, position: Int) {

+ 11 - 34
app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt

@@ -22,18 +22,17 @@ package com.nextcloud.talk.adapters
 
 import android.text.TextUtils
 import androidx.recyclerview.widget.RecyclerView
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.interfaces.DraweeController
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ReactionItemBinding
+import com.nextcloud.talk.extensions.loadAvatar
+import com.nextcloud.talk.extensions.loadGuestAvatar
 import com.nextcloud.talk.models.json.reactions.ReactionVoter
-import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 
 class ReactionsViewHolder(
     private val binding: ReactionItemBinding,
-    private val baseUrl: String?
+    private val user: User?
 ) : RecyclerView.ViewHolder(binding.root) {
 
     fun bind(reactionItem: ReactionItem, clickListener: ReactionItemClickListener) {
@@ -41,7 +40,7 @@ class ReactionsViewHolder(
         binding.reaction.text = reactionItem.reaction
         binding.name.text = reactionItem.reactionVoter.actorDisplayName
 
-        if (baseUrl != null && baseUrl.isNotEmpty()) {
+        if (user != null && user.baseUrl?.isNotEmpty() == true) {
             loadAvatar(reactionItem)
         }
     }
@@ -52,35 +51,13 @@ class ReactionsViewHolder(
             if (!TextUtils.isEmpty(reactionItem.reactionVoter.actorDisplayName)) {
                 displayName = reactionItem.reactionVoter.actorDisplayName!!
             }
-            val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
-                .setOldController(binding.avatar.controller)
-                .setAutoPlayAnimations(true)
-                .setImageRequest(
-                    DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForGuestAvatar(
-                            baseUrl,
-                            displayName,
-                            false
-                        )
-                    )
-                )
-                .build()
-            binding.avatar.controller = draweeController
+            binding.avatar.loadGuestAvatar(user!!.baseUrl!!, displayName!!, false)
         } else if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.USERS) {
-            val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
-                .setOldController(binding.avatar.controller)
-                .setAutoPlayAnimations(true)
-                .setImageRequest(
-                    DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForAvatar(
-                            baseUrl,
-                            reactionItem.reactionVoter.actorId,
-                            false
-                        )
-                    )
-                )
-                .build()
-            binding.avatar.controller = draweeController
+            binding.avatar.loadAvatar(
+                user!!,
+                reactionItem.reactionVoter.actorId!!,
+                false
+            )
         }
     }
 }

+ 2 - 20
app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java

@@ -27,15 +27,12 @@ import android.net.Uri;
 import android.text.TextUtils;
 import android.view.View;
 
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.AccountItemBinding;
+import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.ui.theme.ViewThemeUtils;
-import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.DisplayUtils;
 
 import java.util.List;
 import java.util.regex.Pattern;
@@ -108,7 +105,6 @@ public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.User
 
     @Override
     public void bindViewHolder(FlexibleAdapter adapter, UserItemViewHolder holder, int position, List payloads) {
-        holder.binding.userIcon.setController(null);
 
         if (adapter.hasFilter()) {
             FlexibleUtils.highlightText(
@@ -129,24 +125,10 @@ public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.User
             }
         }
 
-        holder.binding.userIcon.getHierarchy().setPlaceholderImage(R.drawable.account_circle_48dp);
-        holder.binding.userIcon.getHierarchy().setFailureImage(R.drawable.account_circle_48dp);
-
         if (user != null && user.getBaseUrl() != null &&
                 user.getBaseUrl().startsWith("http://") ||
                 user.getBaseUrl().startsWith("https://")) {
-
-            DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                .setOldController(holder.binding.userIcon.getController())
-                .setAutoPlayAnimations(true)
-                .setImageRequest(
-                    DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForAvatar(
-                            user.getBaseUrl(),
-                            participant.getCalculatedActorId(),
-                            true)))
-                .build();
-            holder.binding.userIcon.setController(draweeController);
+            ImageViewExtensionsKt.loadAvatar(holder.binding.userIcon, user, participant.getCalculatedActorId(), true);
         }
     }
 

+ 20 - 33
app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java

@@ -29,16 +29,13 @@ import android.os.Build;
 import android.text.TextUtils;
 import android.view.View;
 
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.RvItemContactBinding;
+import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.ui.theme.ViewThemeUtils;
-import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.DisplayUtils;
 
 import java.util.List;
 import java.util.regex.Pattern;
@@ -110,7 +107,6 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
     @SuppressLint("SetTextI18n")
     @Override
     public void bindViewHolder(FlexibleAdapter adapter, ContactItemViewHolder holder, int position, List payloads) {
-        holder.binding.avatarDraweeView.setController(null);
 
         if (participant.getSelected()) {
             viewThemeUtils.platform.colorImageView(holder.binding.checkedImageView);
@@ -121,18 +117,18 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
 
         if (!isOnline) {
             holder.binding.nameText.setTextColor(ResourcesCompat.getColor(
-                holder.binding.nameText.getContext().getResources(),
-                R.color.medium_emphasis_text,
-                null)
+                                                     holder.binding.nameText.getContext().getResources(),
+                                                     R.color.medium_emphasis_text,
+                                                     null)
                                                 );
-            holder.binding.avatarDraweeView.setAlpha(0.38f);
+            holder.binding.avatarView.setAlpha(0.38f);
         } else {
             holder.binding.nameText.setTextColor(ResourcesCompat.getColor(
-                holder.binding.nameText.getContext().getResources(),
-                R.color.high_emphasis_text,
-                null)
+                                                     holder.binding.nameText.getContext().getResources(),
+                                                     R.color.high_emphasis_text,
+                                                     null)
                                                 );
-            holder.binding.avatarDraweeView.setAlpha(1.0f);
+            holder.binding.avatarView.setAlpha(1.0f);
         }
 
         holder.binding.nameText.setText(participant.getDisplayName());
@@ -179,38 +175,29 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
                     .getResources().getString(R.string.nc_guest);
             }
 
-            setUserStyleAvatar(holder,
-                               ApiUtils.getUrlForGuestAvatar(user.getBaseUrl(), displayName, false));
+            ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, user, displayName, true);
         } else if (participant.getCalculatedActorType() == Participant.ActorType.USERS ||
             PARTICIPANT_SOURCE_USERS.equals(participant.getSource())) {
-            setUserStyleAvatar(holder,
-                               ApiUtils.getUrlForAvatar(user.getBaseUrl(),
-                                                        participant.getCalculatedActorId(),
-                                                        false));
+            ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, user, participant.getCalculatedActorId(), true);
         }
     }
 
-    private void setUserStyleAvatar(ContactItemViewHolder holder, String avatarUrl) {
-        DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-            .setOldController(holder.binding.avatarDraweeView.getController())
-            .setAutoPlayAnimations(true)
-            .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarUrl))
-            .build();
-        holder.binding.avatarDraweeView.setController(draweeController);
-    }
-
     private void setGenericAvatar(
         ContactItemViewHolder holder,
         int roundPlaceholderDrawable,
         int fallbackImageResource) {
+        Object avatar;
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            holder.binding.avatarDraweeView.getHierarchy().setPlaceholderImage(
-                DisplayUtils.getRoundedDrawable(
-                    viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.avatarDraweeView,
-                                                          roundPlaceholderDrawable)));
+            avatar = viewThemeUtils.talk.themePlaceholderAvatar(
+                holder.binding.avatarView,
+                roundPlaceholderDrawable
+                                                               );
+
         } else {
-            holder.binding.avatarDraweeView.setImageResource(fallbackImageResource);
+            avatar = fallbackImageResource;
         }
+
+        ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, avatar);
     }
 
     @Override

+ 133 - 152
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt

@@ -29,27 +29,26 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.content.res.ColorStateList
 import android.graphics.Typeface
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
 import android.os.Build
 import android.text.TextUtils
 import android.text.format.DateUtils
 import android.view.View
 import androidx.core.content.ContextCompat
 import androidx.core.content.res.ResourcesCompat
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.interfaces.DraweeController
 import com.nextcloud.talk.R
 import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding
+import com.nextcloud.talk.extensions.loadAvatar
+import com.nextcloud.talk.extensions.loadGroupCallAvatar
+import com.nextcloud.talk.extensions.loadPublicCallAvatar
+import com.nextcloud.talk.extensions.loadSystemAvatar
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType
 import com.nextcloud.talk.ui.StatusDrawable
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
 import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -113,7 +112,6 @@ class ConversationItem(
         payloads: List<Any>
     ) {
         val appContext = sharedApplication!!.applicationContext
-        holder.binding.dialogAvatar.controller = null
         holder.binding.dialogName.setTextColor(
             ResourcesCompat.getColor(
                 context.resources,
@@ -130,40 +128,7 @@ class ConversationItem(
             holder.binding.dialogName.text = model.displayName
         }
         if (model.unreadMessages > 0) {
-            holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD)
-            holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.typeface, Typeface.BOLD)
-            holder.binding.dialogUnreadBubble.visibility = View.VISIBLE
-            if (model.unreadMessages < 1000) {
-                holder.binding.dialogUnreadBubble.text = model.unreadMessages.toLong().toString()
-            } else {
-                holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages)
-            }
-            val lightBubbleFillColor = ColorStateList.valueOf(
-                ContextCompat.getColor(
-                    context,
-                    R.color.conversation_unread_bubble
-                )
-            )
-            val lightBubbleTextColor = ContextCompat.getColor(
-                context,
-                R.color.conversation_unread_bubble_text
-            )
-            if (model.type === ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
-                viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
-            } else if (model.unreadMention) {
-                if (hasSpreedFeatureCapability(user, "direct-mention-flag")) {
-                    if (model.unreadMentionDirect!!) {
-                        viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
-                    } else {
-                        viewThemeUtils.material.colorChipOutlined(holder.binding.dialogUnreadBubble, 6.0f)
-                    }
-                } else {
-                    viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
-                }
-            } else {
-                holder.binding.dialogUnreadBubble.chipBackgroundColor = lightBubbleFillColor
-                holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor)
-            }
+            showUnreadMessages(holder)
         } else {
             holder.binding.dialogName.setTypeface(null, Typeface.NORMAL)
             holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL)
@@ -175,13 +140,13 @@ class ConversationItem(
         } else {
             holder.binding.favoriteConversationImageView.visibility = View.GONE
         }
-        if (model != null && ConversationType.ROOM_SYSTEM !== model.type) {
+        if (ConversationType.ROOM_SYSTEM !== model.type) {
             val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext)
             holder.binding.userStatusImage.visibility = View.VISIBLE
             holder.binding.userStatusImage.setImageDrawable(
                 StatusDrawable(
                     model.status,
-                    model.status,
+                    model.statusIcon,
                     size,
                     context.resources.getColor(R.color.bg_default),
                     appContext
@@ -190,150 +155,163 @@ class ConversationItem(
         } else {
             holder.binding.userStatusImage.visibility = View.GONE
         }
-        if (model.lastMessage != null) {
-            holder.binding.dialogDate.visibility = View.VISIBLE
-            holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString(
-                model.lastActivity * 1000L,
-                System.currentTimeMillis(),
-                0,
-                DateUtils.FORMAT_ABBREV_RELATIVE
-            )
-            if (!TextUtils.isEmpty(model.lastMessage!!.systemMessage) ||
-                ConversationType.ROOM_SYSTEM === model.type
-            ) {
-                holder.binding.dialogLastMessage.text = model.lastMessage!!.text
-            } else {
-                model.lastMessage!!.activeUser = user
-                val text: String
-                if (model.lastMessage!!.getCalculateMessageType() === ChatMessage.MessageType.REGULAR_TEXT_MESSAGE) {
-                    if (model.lastMessage!!.actorId == user.userId) {
-                        text = String.format(
-                            appContext.getString(R.string.nc_formatted_message_you),
-                            model.lastMessage!!.lastMessageDisplayText
-                        )
-                    } else {
-                        val authorDisplayName =
-                            if (!TextUtils.isEmpty(model.lastMessage!!.actorDisplayName)) {
-                                model.lastMessage!!.actorDisplayName
-                            } else if ("guests" == model.lastMessage!!.actorType) {
-                                appContext.getString(R.string.nc_guest)
-                            } else {
-                                ""
-                            }
-                        text = String.format(
-                            appContext.getString(R.string.nc_formatted_message),
-                            authorDisplayName,
-                            model.lastMessage!!.lastMessageDisplayText
-                        )
-                    }
+        setLastMessage(holder, appContext)
+        showAvatar(holder)
+    }
+
+    private fun showAvatar(holder: ConversationItemViewHolder) {
+        holder.binding.dialogAvatar.visibility = View.VISIBLE
+        var shouldLoadAvatar = shouldLoadAvatar(holder)
+        if (ConversationType.ROOM_SYSTEM == model.type) {
+            holder.binding.dialogAvatar.loadSystemAvatar()
+            shouldLoadAvatar = false
+        }
+        if (shouldLoadAvatar) {
+            when (model.type) {
+                ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(model.name)) {
+                    holder.binding.dialogAvatar.loadAvatar(user, model.name!!)
                 } else {
-                    text = model.lastMessage!!.lastMessageDisplayText
+                    holder.binding.dialogAvatar.visibility = View.GONE
                 }
-                holder.binding.dialogLastMessage.text = text
+                ConversationType.ROOM_GROUP_CALL ->
+                    holder.binding.dialogAvatar.loadGroupCallAvatar(viewThemeUtils)
+                ConversationType.ROOM_PUBLIC_CALL ->
+                    holder.binding.dialogAvatar.loadPublicCallAvatar(viewThemeUtils)
+                else -> holder.binding.dialogAvatar.visibility = View.GONE
             }
-        } else {
-            holder.binding.dialogDate.visibility = View.GONE
-            holder.binding.dialogLastMessage.setText(R.string.nc_no_messages_yet)
         }
-        holder.binding.dialogAvatar.visibility = View.VISIBLE
-        var shouldLoadAvatar = true
+    }
+
+    private fun shouldLoadAvatar(
+        holder: ConversationItemViewHolder
+    ): Boolean {
         var objectType: String?
+        var returnValue = true
         if (!TextUtils.isEmpty(model.objectType.also { objectType = it })) {
             when (objectType) {
                 "share:password" -> {
-                    shouldLoadAvatar = false
                     holder.binding.dialogAvatar.setImageDrawable(
                         ContextCompat.getDrawable(
                             context,
                             R.drawable.ic_circular_lock
                         )
                     )
+                    returnValue = false
                 }
                 "file" -> {
-                    shouldLoadAvatar = false
                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                        holder.binding.dialogAvatar.setImageDrawable(
-                            DisplayUtils.getRoundedDrawable(
-                                viewThemeUtils.talk.themePlaceholderAvatar(
-                                    holder.binding.dialogAvatar,
-                                    R.drawable.ic_avatar_document
-                                )
+                        holder.binding.dialogAvatar.loadAvatar(
+                            viewThemeUtils.talk.themePlaceholderAvatar(
+                                holder.binding.dialogAvatar,
+                                R.drawable.ic_avatar_document
                             )
                         )
                     } else {
-                        holder.binding.dialogAvatar.setImageDrawable(
-                            ContextCompat.getDrawable(context, R.drawable.ic_circular_document)
+                        holder.binding.dialogAvatar.loadAvatar(
+                            R.drawable.ic_circular_document
                         )
                     }
+                    returnValue = false
                 }
-                else -> {}
             }
         }
-        if (ConversationType.ROOM_SYSTEM == model.type) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
-                layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
-                val layerDrawable = LayerDrawable(layers)
-                holder.binding.dialogAvatar.hierarchy.setPlaceholderImage(
-                    DisplayUtils.getRoundedDrawable(layerDrawable)
-                )
+        return returnValue
+    }
+
+    private fun setLastMessage(
+        holder: ConversationItemViewHolder,
+        appContext: Context
+    ) {
+        if (model.lastMessage != null) {
+            holder.binding.dialogDate.visibility = View.VISIBLE
+            holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString(
+                model.lastActivity * MILLIES,
+                System.currentTimeMillis(),
+                0,
+                DateUtils.FORMAT_ABBREV_RELATIVE
+            )
+            if (!TextUtils.isEmpty(model.lastMessage!!.systemMessage) ||
+                ConversationType.ROOM_SYSTEM === model.type
+            ) {
+                holder.binding.dialogLastMessage.text = model.lastMessage!!.text
             } else {
-                holder.binding.dialogAvatar.hierarchy.setPlaceholderImage(R.mipmap.ic_launcher)
-            }
-            shouldLoadAvatar = false
-        }
-        if (shouldLoadAvatar) {
-            when (model.type) {
-                ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(model.name)) {
-                    val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
-                        .setOldController(holder.binding.dialogAvatar.controller)
-                        .setAutoPlayAnimations(true)
-                        .setImageRequest(
-                            DisplayUtils.getImageRequestForUrl(
-                                ApiUtils.getUrlForAvatar(
-                                    user.baseUrl,
-                                    model.name,
-                                    true
-                                ),
-                                user
-                            )
-                        )
-                        .build()
-                    holder.binding.dialogAvatar.controller = draweeController
+                model.lastMessage!!.activeUser = user
+
+                val text = if (model.lastMessage!!.getCalculateMessageType() === ChatMessage.MessageType
+                    .REGULAR_TEXT_MESSAGE
+                ) {
+                    calculateRegularLastMessageText(appContext)
                 } else {
-                    holder.binding.dialogAvatar.visibility = View.GONE
+                    model.lastMessage!!.lastMessageDisplayText
                 }
-                ConversationType.ROOM_GROUP_CALL -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    holder.binding.dialogAvatar.setImageDrawable(
-                        DisplayUtils.getRoundedDrawable(
-                            viewThemeUtils.talk.themePlaceholderAvatar(
-                                holder.binding.dialogAvatar,
-                                R.drawable.ic_avatar_group
-                            )
-                        )
-                    )
+                holder.binding.dialogLastMessage.text = text
+            }
+        } else {
+            holder.binding.dialogDate.visibility = View.GONE
+            holder.binding.dialogLastMessage.setText(R.string.nc_no_messages_yet)
+        }
+    }
+
+    private fun calculateRegularLastMessageText(appContext: Context): String {
+        return if (model.lastMessage!!.actorId == user.userId) {
+            String.format(
+                appContext.getString(R.string.nc_formatted_message_you),
+                model.lastMessage!!.lastMessageDisplayText
+            )
+        } else {
+            val authorDisplayName =
+                if (!TextUtils.isEmpty(model.lastMessage!!.actorDisplayName)) {
+                    model.lastMessage!!.actorDisplayName
+                } else if ("guests" == model.lastMessage!!.actorType) {
+                    appContext.getString(R.string.nc_guest)
                 } else {
-                    holder.binding.dialogAvatar.setImageDrawable(
-                        ContextCompat.getDrawable(context, R.drawable.ic_circular_group)
-                    )
+                    ""
                 }
-                ConversationType.ROOM_PUBLIC_CALL -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    holder.binding.dialogAvatar.setImageDrawable(
-                        DisplayUtils.getRoundedDrawable(
-                            viewThemeUtils.talk.themePlaceholderAvatar(
-                                holder.binding.dialogAvatar,
-                                R.drawable.ic_avatar_link
-                            )
-                        )
-                    )
+            String.format(
+                appContext.getString(R.string.nc_formatted_message),
+                authorDisplayName,
+                model.lastMessage!!.lastMessageDisplayText
+            )
+        }
+    }
+
+    private fun showUnreadMessages(holder: ConversationItemViewHolder) {
+        holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD)
+        holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.typeface, Typeface.BOLD)
+        holder.binding.dialogUnreadBubble.visibility = View.VISIBLE
+        if (model.unreadMessages < UNREAD_MESSAGES_TRESHOLD) {
+            holder.binding.dialogUnreadBubble.text = model.unreadMessages.toLong().toString()
+        } else {
+            holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages)
+        }
+        val lightBubbleFillColor = ColorStateList.valueOf(
+            ContextCompat.getColor(
+                context,
+                R.color.conversation_unread_bubble
+            )
+        )
+        val lightBubbleTextColor = ContextCompat.getColor(
+            context,
+            R.color.conversation_unread_bubble_text
+        )
+        if (model.type === ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
+            viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
+        } else if (model.unreadMention) {
+            if (hasSpreedFeatureCapability(user, "direct-mention-flag")) {
+                if (model.unreadMentionDirect!!) {
+                    viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
                 } else {
-                    holder.binding.dialogAvatar.setImageDrawable(
-                        ContextCompat.getDrawable(context, R.drawable.ic_circular_link)
+                    viewThemeUtils.material.colorChipOutlined(
+                        holder.binding.dialogUnreadBubble,
+                        UNREAD_BUBBLE_STROKE_WIDTH
                     )
                 }
-                else -> holder.binding.dialogAvatar.visibility = View.GONE
+            } else {
+                viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
             }
+        } else {
+            holder.binding.dialogUnreadBubble.chipBackgroundColor = lightBubbleFillColor
+            holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor)
         }
     }
 
@@ -363,6 +341,9 @@ class ConversationItem(
 
     companion object {
         const val VIEW_TYPE = R.layout.rv_item_conversation_with_last_message
+        private const val MILLIES = 1000L
         private const val STATUS_SIZE_IN_DP = 9f
+        private const val UNREAD_BUBBLE_STROKE_WIDTH = 6.0f
+        private const val UNREAD_MESSAGES_TRESHOLD = 1000
     }
 }

+ 10 - 24
app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java

@@ -29,15 +29,13 @@ import android.content.Context;
 import android.os.Build;
 import android.view.View;
 
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.data.user.model.User;
+import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
 import com.nextcloud.talk.models.json.mention.Mention;
 import com.nextcloud.talk.models.json.status.StatusType;
 import com.nextcloud.talk.ui.StatusDrawable;
 import com.nextcloud.talk.ui.theme.ViewThemeUtils;
-import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 
 import java.util.List;
@@ -151,34 +149,22 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<ParticipantIte
 
         if (SOURCE_CALLS.equals(source)) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                holder.binding.avatarDraweeView.getHierarchy().setPlaceholderImage(
-                    DisplayUtils.getRoundedDrawable(
-                        viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.avatarDraweeView,
-                                                              R.drawable.ic_avatar_group)));
+                ImageViewExtensionsKt.loadAvatar(
+                    holder.binding.avatarView,
+                    viewThemeUtils.talk.themePlaceholderAvatar(
+                        holder.binding.avatarView,
+                        R.drawable.ic_avatar_group
+                                                              )
+                                                );
             } else {
-                holder.binding.avatarDraweeView.setImageResource(R.drawable.ic_circular_group);
+                ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, R.drawable.ic_circular_group);
             }
         } else {
             String avatarId = objectId;
-            String avatarUrl = ApiUtils.getUrlForAvatar(currentUser.getBaseUrl(),
-                                                        avatarId, true);
-
             if (SOURCE_GUESTS.equals(source)) {
                 avatarId = displayName;
-                avatarUrl = ApiUtils.getUrlForGuestAvatar(
-                    currentUser.getBaseUrl(),
-                    avatarId,
-                    false);
             }
-
-            holder.binding.avatarDraweeView.setController(null);
-
-            DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                .setOldController(holder.binding.avatarDraweeView.getController())
-                .setAutoPlayAnimations(true)
-                .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarUrl))
-                .build();
-            holder.binding.avatarDraweeView.setController(draweeController);
+            ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, currentUser, avatarId, true);
         }
 
         drawStatus(holder);

+ 4 - 13
app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Álvaro Brey
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2022 Álvaro Brey
  * Copyright (C) 2022 Nextcloud GmbH
  *
@@ -27,9 +29,9 @@ import androidx.recyclerview.widget.RecyclerView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.RvItemSearchMessageBinding
+import com.nextcloud.talk.extensions.loadThumbnail
 import com.nextcloud.talk.models.domain.SearchMessageEntry
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.flexibleadapter.items.IFilterable
@@ -72,7 +74,7 @@ data class MessageResultItem constructor(
     ) {
         holder.binding.conversationTitle.text = messageEntry.title
         bindMessageExcerpt(holder)
-        loadImage(holder)
+        messageEntry.thumbnailURL?.let { holder.binding.thumbnail.loadThumbnail(it, currentUser) }
     }
 
     private fun bindMessageExcerpt(holder: ViewHolder) {
@@ -83,17 +85,6 @@ data class MessageResultItem constructor(
         )
     }
 
-    private fun loadImage(holder: ViewHolder) {
-        DisplayUtils.loadAvatarPlaceholder(holder.binding.thumbnail)
-        if (messageEntry.thumbnailURL != null) {
-            val imageRequest = DisplayUtils.getImageRequestForUrl(
-                messageEntry.thumbnailURL,
-                currentUser
-            )
-            DisplayUtils.loadImage(holder.binding.thumbnail, imageRequest)
-        }
-    }
-
     override fun filter(constraint: String?): Boolean = true
 
     override fun getItemViewType(): Int {

+ 13 - 46
app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java

@@ -27,23 +27,20 @@ package com.nextcloud.talk.adapters.items;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
-import android.os.Build;
 import android.text.TextUtils;
 import android.view.View;
 
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.RvItemConversationInfoParticipantBinding;
+import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
 import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.models.json.participants.Participant.InCallFlags;
 import com.nextcloud.talk.models.json.status.StatusType;
 import com.nextcloud.talk.ui.StatusDrawable;
 import com.nextcloud.talk.ui.theme.ViewThemeUtils;
-import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 
 import java.util.List;
@@ -111,24 +108,22 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
     @Override
     public void bindViewHolder(FlexibleAdapter adapter, ParticipantItemViewHolder holder, int position, List payloads) {
 
-        holder.binding.avatarDraweeView.setController(null);
-
         drawStatus(holder);
 
         if (!isOnline) {
             holder.binding.nameText.setTextColor(ResourcesCompat.getColor(
-                holder.binding.nameText.getContext().getResources(),
-                R.color.medium_emphasis_text,
-                null)
+                                                     holder.binding.nameText.getContext().getResources(),
+                                                     R.color.medium_emphasis_text,
+                                                     null)
                                                 );
-            holder.binding.avatarDraweeView.setAlpha(0.38f);
+            holder.binding.avatarView.setAlpha(0.38f);
         } else {
             holder.binding.nameText.setTextColor(ResourcesCompat.getColor(
-                holder.binding.nameText.getContext().getResources(),
-                R.color.high_emphasis_text,
-                null)
+                                                     holder.binding.nameText.getContext().getResources(),
+                                                     R.color.high_emphasis_text,
+                                                     null)
                                                 );
-            holder.binding.avatarDraweeView.setAlpha(1.0f);
+            holder.binding.avatarView.setAlpha(1.0f);
         }
 
         holder.binding.nameText.setText(participant.getDisplayName());
@@ -152,23 +147,9 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
             "groups".equals(participant.getSource()) ||
             participant.getCalculatedActorType() == Participant.ActorType.CIRCLES ||
             "circles".equals(participant.getSource())) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                holder.binding.avatarDraweeView.getHierarchy().setPlaceholderImage(
-                    DisplayUtils.getRoundedDrawable(
-                        viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.avatarDraweeView,
-                                                              R.drawable.ic_avatar_group)));
-            } else {
-                holder.binding.avatarDraweeView.setImageResource(R.drawable.ic_circular_group);
-            }
+            ImageViewExtensionsKt.loadGroupCallAvatar(holder.binding.avatarView, viewThemeUtils);
         } else if (participant.getCalculatedActorType() == Participant.ActorType.EMAILS) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                holder.binding.avatarDraweeView.getHierarchy().setPlaceholderImage(
-                    DisplayUtils.getRoundedDrawable(
-                        viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.avatarDraweeView,
-                                                              R.drawable.ic_avatar_mail)));
-            } else {
-                holder.binding.avatarDraweeView.setImageResource(R.drawable.ic_circular_mail);
-            }
+            ImageViewExtensionsKt.loadMailAvatar(holder.binding.avatarView, viewThemeUtils);
         } else if (participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
             participant.getType() == Participant.ParticipantType.GUEST ||
             participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
@@ -180,25 +161,11 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
                 displayName = participant.getDisplayName();
             }
 
-            DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                .setOldController(holder.binding.avatarDraweeView.getController())
-                .setAutoPlayAnimations(true)
-                .setImageRequest(DisplayUtils.getImageRequestForUrl(
-                    ApiUtils.getUrlForGuestAvatar(user.getBaseUrl(),
-                                                  displayName, false)))
-                .build();
-            holder.binding.avatarDraweeView.setController(draweeController);
+            ImageViewExtensionsKt.loadGuestAvatar(holder.binding.avatarView, user, displayName, false);
 
         } else if (participant.getCalculatedActorType() == Participant.ActorType.USERS ||
             participant.getSource().equals("users")) {
-            DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                .setOldController(holder.binding.avatarDraweeView.getController())
-                .setAutoPlayAnimations(true)
-                .setImageRequest(DisplayUtils.getImageRequestForUrl(
-                    ApiUtils.getUrlForAvatar(user.getBaseUrl(),
-                                             participant.getCalculatedActorId(), false)))
-                .build();
-            holder.binding.avatarDraweeView.setController(draweeController);
+            ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, user, participant.getCalculatedActorId(), true);
         }
 
         Resources resources = NextcloudTalkApplication.Companion.getSharedApplication().getResources();

+ 4 - 25
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt

@@ -24,25 +24,21 @@ package com.nextcloud.talk.adapters.messages
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
-import android.os.Build
 import android.text.TextUtils
 import android.view.View
 import androidx.core.content.ContextCompat
-import androidx.core.content.res.ResourcesCompat
 import autodagger.AutoInjector
 import coil.load
-import com.amulyakhare.textdrawable.TextDrawable
 import com.nextcloud.talk.R
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding
+import com.nextcloud.talk.extensions.loadBotsAvatar
+import com.nextcloud.talk.extensions.loadChangelogBotAvatar
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.stfalcon.chatkit.messages.MessageHolders
 import javax.inject.Inject
@@ -143,26 +139,9 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : M
         if (message.actorType == "guests") {
             // do nothing, avatar is set
         } else if (message.actorType == "bots" && message.actorId == "changelog") {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
-                layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
-                val layerDrawable = LayerDrawable(layers)
-                binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
-            } else {
-                binding.messageUserAvatar.setImageResource(R.mipmap.ic_launcher)
-            }
+            binding.messageUserAvatar.loadChangelogBotAvatar()
         } else if (message.actorType == "bots") {
-            val drawable = TextDrawable.builder()
-                .beginConfig()
-                .bold()
-                .endConfig()
-                .buildRound(
-                    ">",
-                    ResourcesCompat.getColor(context.resources, R.color.black, null)
-                )
-            binding.messageUserAvatar.visibility = View.VISIBLE
-            binding.messageUserAvatar.setImageDrawable(drawable)
+            binding.messageUserAvatar.loadBotsAvatar()
         }
     }
 

+ 4 - 20
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt

@@ -29,8 +29,6 @@ package com.nextcloud.talk.adapters.messages
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
 import android.net.Uri
 import android.text.TextUtils
 import android.util.Log
@@ -40,18 +38,17 @@ import android.view.View
 import android.webkit.WebView
 import android.webkit.WebViewClient
 import android.widget.Toast
-import androidx.appcompat.content.res.AppCompatResources
 import autodagger.AutoInjector
 import coil.load
-import com.amulyakhare.textdrawable.TextDrawable
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
+import com.nextcloud.talk.extensions.loadBotsAvatar
+import com.nextcloud.talk.extensions.loadChangelogBotAvatar
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.UriUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.stfalcon.chatkit.messages.MessageHolders
@@ -136,22 +133,9 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
             if (message.actorType == "guests") {
                 // do nothing, avatar is set
             } else if (message.actorType == "bots" && message.actorId == "changelog") {
-                val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = AppCompatResources.getDrawable(context!!, R.drawable.ic_launcher_background)
-                layers[1] = AppCompatResources.getDrawable(context!!, R.drawable.ic_launcher_foreground)
-                val layerDrawable = LayerDrawable(layers)
-                binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
+                binding.messageUserAvatar.loadChangelogBotAvatar()
             } else if (message.actorType == "bots") {
-                val drawable = TextDrawable.builder()
-                    .beginConfig()
-                    .bold()
-                    .endConfig()
-                    .buildRound(
-                        ">",
-                        context!!.resources.getColor(R.color.black)
-                    )
-                binding.messageUserAvatar.visibility = View.VISIBLE
-                binding.messageUserAvatar.setImageDrawable(drawable)
+                binding.messageUserAvatar.loadBotsAvatar()
             }
         } else {
             if (message.isOneToOneConversation) {

+ 4 - 25
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt

@@ -22,27 +22,23 @@ package com.nextcloud.talk.adapters.messages
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
-import android.os.Build
 import android.text.TextUtils
 import android.view.View
 import androidx.core.content.ContextCompat
-import androidx.core.content.res.ResourcesCompat
 import autodagger.AutoInjector
 import coil.load
-import com.amulyakhare.textdrawable.TextDrawable
 import com.nextcloud.talk.R
 import com.nextcloud.talk.activities.MainActivity
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding
+import com.nextcloud.talk.extensions.loadBotsAvatar
+import com.nextcloud.talk.extensions.loadChangelogBotAvatar
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.polls.ui.PollMainDialogFragment
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.stfalcon.chatkit.messages.MessageHolders
 import javax.inject.Inject
@@ -170,26 +166,9 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : MessageH
         if (message.actorType == "guests") {
             // do nothing, avatar is set
         } else if (message.actorType == "bots" && message.actorId == "changelog") {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
-                layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
-                val layerDrawable = LayerDrawable(layers)
-                binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
-            } else {
-                binding.messageUserAvatar.setImageResource(R.mipmap.ic_launcher)
-            }
+            binding.messageUserAvatar.loadChangelogBotAvatar()
         } else if (message.actorType == "bots") {
-            val drawable = TextDrawable.builder()
-                .beginConfig()
-                .bold()
-                .endConfig()
-                .buildRound(
-                    ">",
-                    ResourcesCompat.getColor(context.resources, R.color.black, null)
-                )
-            binding.messageUserAvatar.visibility = View.VISIBLE
-            binding.messageUserAvatar.setImageDrawable(drawable)
+            binding.messageUserAvatar.loadBotsAvatar()
         }
     }
 

+ 3 - 3
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java

@@ -3,7 +3,7 @@
  *
  * @author Andy Scherzinger
  * @author Tim Krüger
- * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2021-2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -23,9 +23,9 @@
 package com.nextcloud.talk.adapters.messages;
 
 import android.view.View;
+import android.widget.ImageView;
 import android.widget.ProgressBar;
 
-import com.facebook.drawee.view.SimpleDraweeView;
 import com.google.android.material.card.MaterialCardView;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
@@ -74,7 +74,7 @@ public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder {
     }
 
     @Override
-    public SimpleDraweeView getPreviewContactPhoto() {
+    public ImageView getPreviewContactPhoto() {
         return binding.contactPhoto;
     }
 

+ 4 - 26
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt

@@ -26,24 +26,21 @@ package com.nextcloud.talk.adapters.messages
 
 import android.content.Context
 import android.content.Intent
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
 import android.net.Uri
-import android.os.Build
 import android.text.Spannable
 import android.text.SpannableString
 import android.text.TextUtils
 import android.util.TypedValue
 import android.view.View
 import androidx.core.content.ContextCompat
-import androidx.core.content.res.ResourcesCompat
 import autodagger.AutoInjector
 import coil.load
-import com.amulyakhare.textdrawable.TextDrawable
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding
+import com.nextcloud.talk.extensions.loadBotsAvatar
+import com.nextcloud.talk.extensions.loadChangelogBotAvatar
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
@@ -179,28 +176,9 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : MessageHolde
         if (message.actorType == "guests") {
             // do nothing, avatar is set
         } else if (message.actorType == "bots" && message.actorId == "changelog") {
-            if (context != null) {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    val layers = arrayOfNulls<Drawable>(2)
-                    layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background)
-                    layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground)
-                    val layerDrawable = LayerDrawable(layers)
-                    binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
-                } else {
-                    binding.messageUserAvatar.setImageResource(R.mipmap.ic_launcher)
-                }
-            }
+            binding.messageUserAvatar.loadChangelogBotAvatar()
         } else if (message.actorType == "bots") {
-            val drawable = TextDrawable.builder()
-                .beginConfig()
-                .bold()
-                .endConfig()
-                .buildRound(
-                    ">",
-                    ResourcesCompat.getColor(context!!.resources, R.color.black, null)
-                )
-            binding.messageUserAvatar.visibility = View.VISIBLE
-            binding.messageUserAvatar.setImageDrawable(drawable)
+            binding.messageUserAvatar.loadBotsAvatar()
         }
     }
 

+ 2 - 13
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -28,9 +28,6 @@ package com.nextcloud.talk.adapters.messages
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
-import android.os.Build
 import android.text.TextUtils
 import android.util.Log
 import android.view.View
@@ -46,10 +43,10 @@ import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
+import com.nextcloud.talk.extensions.loadChangelogBotAvatar
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.stfalcon.chatkit.messages.MessageHolders
 import java.util.concurrent.ExecutionException
@@ -245,15 +242,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
         if (message.actorType == "guests") {
             // do nothing, avatar is set
         } else if (message.actorType == "bots" && message.actorId == "changelog") {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background)
-                layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground)
-                val layerDrawable = LayerDrawable(layers)
-                binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
-            } else {
-                binding.messageUserAvatar.setImageResource(R.mipmap.ic_launcher)
-            }
+            binding.messageUserAvatar.loadChangelogBotAvatar()
         } else if (message.actorType == "bots") {
             val drawable = TextDrawable.builder()
                 .beginConfig()

+ 2 - 9
app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt

@@ -25,14 +25,12 @@ import android.content.Intent
 import android.net.Uri
 import android.util.Log
 import android.view.View
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.interfaces.DraweeController
+import coil.load
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
@@ -92,12 +90,7 @@ class LinkPreview {
                             val referenceThumbUrl = reference.openGraphObject?.thumb
                             if (!referenceThumbUrl.isNullOrEmpty()) {
                                 binding.referenceThumbImage.visibility = View.VISIBLE
-                                val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
-                                    .setAutoPlayAnimations(true)
-                                    .setImageRequest(DisplayUtils.getImageRequestForUrl(referenceThumbUrl))
-                                    .build()
-                                binding.referenceThumbImage.controller =
-                                    draweeController
+                                binding.referenceThumbImage.load(referenceThumbUrl)
                             } else {
                                 binding.referenceThumbImage.visibility = View.GONE
                             }

+ 2 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java

@@ -23,9 +23,9 @@
 package com.nextcloud.talk.adapters.messages;
 
 import android.view.View;
+import android.widget.ImageView;
 import android.widget.ProgressBar;
 
-import com.facebook.drawee.view.SimpleDraweeView;
 import com.google.android.material.card.MaterialCardView;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
@@ -75,7 +75,7 @@ public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder
     }
 
     @Override
-    public SimpleDraweeView getPreviewContactPhoto() {
+    public ImageView getPreviewContactPhoto() {
         return binding.contactPhoto;
     }
 

+ 16 - 18
app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt

@@ -5,7 +5,7 @@
  * @author Marcel Hibbe
  * @author Andy Scherzinger
  * @author Tim Krüger
- * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2021-2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
@@ -30,7 +30,6 @@ import android.content.Context
 import android.content.Intent
 import android.graphics.PorterDuff
 import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
 import android.net.Uri
 import android.os.Handler
 import android.util.Base64
@@ -38,13 +37,13 @@ import android.util.Log
 import android.view.Gravity
 import android.view.MenuItem
 import android.view.View
+import android.widget.ImageView
 import android.widget.PopupMenu
 import android.widget.ProgressBar
 import androidx.appcompat.view.ContextThemeWrapper
 import androidx.core.content.ContextCompat
 import androidx.emoji.widget.EmojiTextView
 import autodagger.AutoInjector
-import com.facebook.drawee.view.SimpleDraweeView
 import com.google.android.material.card.MaterialCardView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
@@ -53,6 +52,7 @@ import com.nextcloud.talk.components.filebrowser.models.BrowserFile
 import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
+import com.nextcloud.talk.extensions.loadChangelogBotAvatar
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
@@ -92,6 +92,8 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
     lateinit var commonMessageInterface: CommonMessageInterface
     var previewMessageInterface: PreviewMessageInterface? = null
 
+    private var placeholder: Drawable? = null
+
     init {
         sharedApplication!!.componentApplication.inject(this)
     }
@@ -118,13 +120,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
                     }
                 }
                 if (ACTOR_TYPE_BOTS == message.actorType && ACTOR_ID_CHANGELOG == message.actorId) {
-                    if (context != null) {
-                        val layers = arrayOfNulls<Drawable>(2)
-                        layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background)
-                        layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground)
-                        val layerDrawable = LayerDrawable(layers)
-                        userAvatar.hierarchy.setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable))
-                    }
+                    userAvatar.loadChangelogBotAvatar()
                 }
             }
         }
@@ -150,11 +146,10 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
             }
             if (message.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_PHOTO)) {
                 image = previewContactPhoto
-                val drawable = getDrawableFromContactDetails(
+                placeholder = getDrawableFromContactDetails(
                     context,
                     message.selectedIndividualHashMap!![KEY_CONTACT_PHOTO]
                 )
-                image.hierarchy.setPlaceholderImage(drawable)
             } else if (message.selectedIndividualHashMap!!.containsKey(KEY_MIMETYPE)) {
                 val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE]
                 val drawableResourceId = getDrawableResourceIdForMimeType(mimetype)
@@ -170,7 +165,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
                         PorterDuff.Mode.SRC_ATOP
                     )
                 }
-                image.hierarchy.setPlaceholderImage(drawable)
+                placeholder = drawable
             } else {
                 fetchFileInformation(
                     "/" + message.selectedIndividualHashMap!![KEY_PATH],
@@ -208,13 +203,13 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
             DisplayUtils.setClickableString("Tenor", "https://tenor.com", messageText)
         } else {
             if (message.messageType == ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE.name) {
-                (clickView as SimpleDraweeView?)?.setOnClickListener {
+                clickView!!.setOnClickListener {
                     val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(message.imageUrl))
                     browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                     context!!.startActivity(browserIntent)
                 }
             } else {
-                (clickView as SimpleDraweeView?)?.setOnClickListener(null)
+                clickView!!.setOnClickListener(null)
             }
             messageText.text = ""
         }
@@ -238,6 +233,10 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
         commonMessageInterface.onClickReaction(chatMessage, emoji)
     }
 
+    override fun getPayloadForImageLoader(message: ChatMessage?): Any? {
+        return placeholder
+    }
+
     private fun getDrawableFromContactDetails(context: Context?, base64: String?): Drawable? {
         var drawable: Drawable? = null
         if (base64 != "") {
@@ -300,8 +299,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
                         if (browserFileList.isNotEmpty()) {
                             Handler(context!!.mainLooper).post {
                                 val resourceId = getDrawableResourceIdForMimeType(browserFileList[0].mimeType)
-                                val drawable = ContextCompat.getDrawable(context!!, resourceId)
-                                image.hierarchy.setPlaceholderImage(drawable)
+                                placeholder = ContextCompat.getDrawable(context!!, resourceId)
                             }
                         }
                     }
@@ -324,7 +322,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
     abstract val messageText: EmojiTextView
     abstract val previewContainer: View
     abstract val previewContactContainer: MaterialCardView
-    abstract val previewContactPhoto: SimpleDraweeView
+    abstract val previewContactPhoto: ImageView
     abstract val previewContactName: EmojiTextView
     abstract val previewContactProgressBar: ProgressBar?
 

+ 10 - 19
app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt

@@ -4,6 +4,8 @@
  * @author Marcel Hibbe
  * @author Andy Scherzinger
  * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
  * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
@@ -46,9 +48,7 @@ import coil.decode.GifDecoder
 import coil.decode.ImageDecoderDecoder
 import coil.decode.SvgDecoder
 import coil.memory.MemoryCache
-import com.facebook.cache.disk.DiskCacheConfig
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.imagepipeline.core.ImagePipelineConfig
+import coil.util.DebugLogger
 import com.nextcloud.talk.BuildConfig
 import com.nextcloud.talk.components.filebrowser.webdav.DavUtils
 import com.nextcloud.talk.dagger.modules.BusModule
@@ -66,7 +66,6 @@ import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.DeviceUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.NotificationUtils
-import com.nextcloud.talk.utils.OkHttpNetworkFetcherWithCache
 import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageModule
 import com.nextcloud.talk.utils.database.user.UserModule
 import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -174,18 +173,6 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
         setAppTheme(appPreferences.theme)
         super.onCreate()
 
-        val imagePipelineConfig = ImagePipelineConfig.newBuilder(this)
-            .setNetworkFetcher(OkHttpNetworkFetcherWithCache(okHttpClient))
-            .setMainDiskCacheConfig(
-                DiskCacheConfig.newBuilder(this)
-                    .setMaxCacheSize(0)
-                    .setMaxCacheSizeOnLowDiskSpace(0)
-                    .setMaxCacheSizeOnVeryLowDiskSpace(0)
-                    .build()
-            )
-            .build()
-
-        Fresco.initialize(this, imagePipelineConfig)
         Security.insertProviderAt(Conscrypt.newProvider(), 1)
 
         ClosedInterfaceImpl().providerInstallerInstallIfNeededAsync()
@@ -240,7 +227,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
     }
 
     private fun buildDefaultImageLoader(): ImageLoader {
-        return ImageLoader.Builder(applicationContext)
+        val imageLoaderBuilder = ImageLoader.Builder(applicationContext)
             .memoryCache {
                 // Use 50% of the application's available memory.
                 MemoryCache.Builder(applicationContext).maxSizePercent(FIFTY_PERCENT).build()
@@ -254,8 +241,12 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
                 }
                 add(SvgDecoder.Factory())
             }
-            .okHttpClient(okHttpClient)
-            .build()
+
+        if (BuildConfig.DEBUG) {
+            imageLoaderBuilder.logger(DebugLogger())
+        }
+
+        return imageLoaderBuilder.build()
     }
 
     companion object {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java

@@ -27,7 +27,7 @@ import android.text.Editable;
 import android.text.Spanned;
 import android.widget.EditText;
 
-import com.facebook.widget.text.span.BetterImageSpan;
+import third.parties.fresco.BetterImageSpan;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.models.json.mention.Mention;

+ 48 - 49
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -5,7 +5,7 @@
  * @author Marcel Hibbe
  * @author Andy Scherzinger
  * @author Tim Krüger
- * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2021-2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
  * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
@@ -37,8 +37,9 @@ import android.content.pm.PackageManager
 import android.content.res.AssetFileDescriptor
 import android.content.res.Resources
 import android.database.Cursor
-import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
 import android.media.MediaPlayer
 import android.media.MediaRecorder
 import android.net.Uri
@@ -78,7 +79,7 @@ import androidx.appcompat.view.ContextThemeWrapper
 import androidx.core.content.ContextCompat
 import androidx.core.content.FileProvider
 import androidx.core.content.PermissionChecker
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
+import androidx.core.graphics.drawable.toBitmap
 import androidx.core.widget.doAfterTextChanged
 import androidx.emoji.text.EmojiCompat
 import androidx.emoji.widget.EmojiTextView
@@ -90,15 +91,13 @@ import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkInfo
 import androidx.work.WorkManager
 import autodagger.AutoInjector
+import coil.imageLoader
 import coil.load
+import coil.request.ImageRequest
+import coil.target.Target
+import coil.transform.CircleCropTransformation
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
-import com.facebook.common.executors.UiThreadImmediateExecutorService
-import com.facebook.common.references.CloseableReference
-import com.facebook.datasource.DataSource
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
-import com.facebook.imagepipeline.image.CloseableImage
 import com.google.android.flexbox.FlexboxLayout
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.nextcloud.talk.BuildConfig
@@ -134,6 +133,7 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ControllerChatBinding
 import com.nextcloud.talk.events.UserMentionClickEvent
 import com.nextcloud.talk.events.WebSocketCommunicationEvent
+import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
 import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
 import com.nextcloud.talk.jobs.ShareOperationWorker
 import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
@@ -165,7 +165,6 @@ import com.nextcloud.talk.utils.ConductorRemapping
 import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
 import com.nextcloud.talk.utils.ContactUtils
 import com.nextcloud.talk.utils.DateUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.FileUtils
 import com.nextcloud.talk.utils.ImageEmojiEditText
 import com.nextcloud.talk.utils.MagicCharPolicy
@@ -462,42 +461,48 @@ class ChatController(args: Bundle) :
 
     private fun loadAvatarForStatusBar() {
         if (isOneToOneConversation() && activity != null) {
-            val imageRequest = DisplayUtils.getImageRequestForUrl(
-                ApiUtils.getUrlForAvatar(
-                    conversationUser?.baseUrl,
-                    currentConversation?.name,
-                    true
-                ),
-                conversationUser!!
+
+            val url = ApiUtils.getUrlForAvatar(
+                conversationUser!!.baseUrl,
+                currentConversation!!.name,
+                true
             )
+            val target = object : Target {
 
-            val imagePipeline = Fresco.getImagePipeline()
-            val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
-
-            dataSource.subscribe(
-                object : BaseBitmapDataSubscriber() {
-                    override fun onNewResultImpl(bitmap: Bitmap?) {
-                        if (actionBar != null && bitmap != null && resources != null) {
-                            val avatarSize = (actionBar?.height!! / TOOLBAR_AVATAR_RATIO).roundToInt()
-                            if (avatarSize > 0) {
-                                val bitmapResized = Bitmap.createScaledBitmap(bitmap, avatarSize, avatarSize, false)
-
-                                val roundedBitmapDrawable =
-                                    RoundedBitmapDrawableFactory.create(resources!!, bitmapResized)
-                                roundedBitmapDrawable.isCircular = true
-                                roundedBitmapDrawable.setAntiAlias(true)
-                                actionBar?.setIcon(roundedBitmapDrawable)
-                            } else {
-                                Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0")
-                            }
+                private fun setIcon(drawable: Drawable?) {
+
+                    actionBar?.let {
+                        val avatarSize = (it.height / TOOLBAR_AVATAR_RATIO).roundToInt()
+
+                        if (drawable != null && avatarSize > 0) {
+                            val bitmap = drawable.toBitmap(avatarSize, avatarSize)
+                            it.setIcon(BitmapDrawable(resources, bitmap))
+                        } else {
+                            Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0")
                         }
                     }
+                }
 
-                    override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
-                        // unused atm
-                    }
-                },
-                UiThreadImmediateExecutorService.getInstance()
+                override fun onStart(placeholder: Drawable?) {
+                    this.setIcon(placeholder)
+                }
+
+                override fun onSuccess(result: Drawable) {
+                    this.setIcon(result)
+                }
+            }
+
+            val credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)
+
+            context.imageLoader.enqueue(
+                ImageRequest.Builder(context)
+                    .data(url)
+                    .addHeader("Authorization", credentials)
+                    .placeholder(R.drawable.ic_user)
+                    .transformations(CircleCropTransformation())
+                    .crossfade(true)
+                    .target(target)
+                    .build()
             )
         }
     }
@@ -617,14 +622,8 @@ class ChatController(args: Bundle) :
             adapter = TalkMessagesListAdapter(
                 senderId,
                 messageHolders,
-                ImageLoader { imageView, url, payload ->
-                    val draweeController = Fresco.newDraweeControllerBuilder()
-                        .setImageRequest(DisplayUtils.getImageRequestForUrl(url, conversationUser))
-                        .setControllerListener(DisplayUtils.getImageControllerListener(imageView))
-                        .setOldController(imageView.controller)
-                        .setAutoPlayAnimations(true)
-                        .build()
-                    imageView.controller = draweeController
+                ImageLoader { imageView, url, _ ->
+                    imageView.loadAvatarOrImagePreview(url!!, conversationUser, placeholder = payload as? Drawable)
                 },
                 this
             )

+ 8 - 48
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -28,9 +28,6 @@ package com.nextcloud.talk.controllers
 
 import android.annotation.SuppressLint
 import android.content.Intent
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.LayerDrawable
-import android.os.Build
 import android.os.Bundle
 import android.os.Parcelable
 import android.text.TextUtils
@@ -42,7 +39,6 @@ import android.view.View.VISIBLE
 import android.widget.Toast
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.widget.SwitchCompat
-import androidx.core.content.ContextCompat
 import androidx.work.Data
 import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkManager
@@ -53,7 +49,6 @@ import com.afollestad.materialdialogs.bottomsheets.BottomSheet
 import com.afollestad.materialdialogs.datetime.dateTimePicker
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
-import com.facebook.drawee.backends.pipeline.Fresco
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.nextcloud.talk.R
 import com.nextcloud.talk.adapters.items.ParticipantItem
@@ -67,6 +62,10 @@ import com.nextcloud.talk.conversation.info.GuestAccessHelper
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ControllerConversationInfoBinding
 import com.nextcloud.talk.events.EventStatus
+import com.nextcloud.talk.extensions.loadAvatar
+import com.nextcloud.talk.extensions.loadSystemAvatar
+import com.nextcloud.talk.extensions.loadGroupCallAvatar
+import com.nextcloud.talk.extensions.loadPublicCallAvatar
 import com.nextcloud.talk.jobs.DeleteConversationWorker
 import com.nextcloud.talk.jobs.LeaveConversationWorker
 import com.nextcloud.talk.models.json.conversations.Conversation
@@ -83,7 +82,6 @@ import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DateConstants
 import com.nextcloud.talk.utils.DateUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys
 import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
 import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
@@ -778,54 +776,16 @@ class ConversationInfoController(args: Bundle) :
     private fun loadConversationAvatar() {
         when (conversation!!.type) {
             Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) {
-                val draweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(binding.avatarImage.controller)
-                    .setAutoPlayAnimations(true)
-                    .setImageRequest(
-                        DisplayUtils.getImageRequestForUrl(
-                            ApiUtils.getUrlForAvatar(
-                                conversationUser!!.baseUrl,
-                                conversation!!.name,
-                                true
-                            ),
-                            conversationUser
-                        )
-                    )
-                    .build()
-                binding.avatarImage.controller = draweeController
+                conversation!!.name?.let { binding.avatarImage.loadAvatar(conversationUser!!, it) }
             }
             Conversation.ConversationType.ROOM_GROUP_CALL -> {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    binding.avatarImage.hierarchy.setPlaceholderImage(
-                        DisplayUtils.getRoundedDrawable(
-                            viewThemeUtils.talk.themePlaceholderAvatar(binding.avatarImage, R.drawable.ic_avatar_group)
-                        )
-                    )
-                } else {
-                    binding.avatarImage.hierarchy.setPlaceholderImage(
-                        R.drawable.ic_circular_group
-                    )
-                }
+                binding.avatarImage.loadGroupCallAvatar(viewThemeUtils)
             }
             Conversation.ConversationType.ROOM_PUBLIC_CALL -> {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    binding.avatarImage.hierarchy.setPlaceholderImage(
-                        DisplayUtils.getRoundedDrawable(
-                            viewThemeUtils.talk.themePlaceholderAvatar(binding.avatarImage, R.drawable.ic_avatar_link)
-                        )
-                    )
-                } else {
-                    binding.avatarImage.hierarchy.setPlaceholderImage(
-                        R.drawable.ic_circular_link
-                    )
-                }
+                binding.avatarImage.loadPublicCallAvatar(viewThemeUtils)
             }
             Conversation.ConversationType.ROOM_SYSTEM -> {
-                val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
-                layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
-                val layerDrawable = LayerDrawable(layers)
-                binding.avatarImage.hierarchy.setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable))
+                binding.avatarImage.loadSystemAvatar()
             }
 
             else -> {

+ 46 - 73
app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt

@@ -31,7 +31,7 @@ import android.app.SearchManager
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
-import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
 import android.net.Uri
 import android.os.Build
 import android.os.Bundle
@@ -49,8 +49,6 @@ import android.view.inputmethod.InputMethodManager
 import android.widget.Toast
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.widget.SearchView
-import androidx.core.content.res.ResourcesCompat
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
 import androidx.core.view.MenuItemCompat
 import androidx.fragment.app.DialogFragment
 import androidx.recyclerview.widget.RecyclerView
@@ -58,15 +56,13 @@ import androidx.work.Data
 import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkManager
 import autodagger.AutoInjector
+import coil.imageLoader
+import coil.request.ImageRequest
+import coil.target.Target
+import coil.transform.CircleCropTransformation
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
 import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
-import com.facebook.common.executors.UiThreadImmediateExecutorService
-import com.facebook.common.references.CloseableReference
-import com.facebook.datasource.DataSource
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
-import com.facebook.imagepipeline.image.CloseableImage
 import com.google.android.material.button.MaterialButton
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.nextcloud.talk.R
@@ -104,7 +100,6 @@ import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.FileUtils
 import com.nextcloud.talk.utils.Mimetype
 import com.nextcloud.talk.utils.ParticipantPermissions
@@ -211,78 +206,56 @@ class ConversationsListController(bundle: Bundle) :
         prepareViews()
     }
 
-    private fun loadUserAvatar(button: MaterialButton) {
+    private fun loadUserAvatar(
+        target: Target
+    ) {
+
         if (activity != null) {
-            val imageRequest = DisplayUtils.getImageRequestForUrl(
-                ApiUtils.getUrlForAvatar(
-                    currentUser!!.baseUrl,
-                    currentUser!!.userId,
-                    true
-                ),
-                currentUser
+            val url = ApiUtils.getUrlForAvatar(
+                currentUser!!.baseUrl,
+                currentUser!!.userId,
+                true
             )
-            val imagePipeline = Fresco.getImagePipeline()
-            val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
-            dataSource.subscribe(
-                object : BaseBitmapDataSubscriber() {
-                    override fun onNewResultImpl(bitmap: Bitmap?) {
-                        if (bitmap != null && resources != null) {
-                            val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(
-                                resources!!,
-                                bitmap
-                            )
-                            roundedBitmapDrawable.isCircular = true
-                            roundedBitmapDrawable.setAntiAlias(true)
-                            button.icon = roundedBitmapDrawable
-                        }
-                    }
 
-                    override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>>) {
-                        if (resources != null) {
-                            button.icon = ResourcesCompat.getDrawable(resources!!, R.drawable.ic_user, null)
-                        }
-                    }
-                },
-                UiThreadImmediateExecutorService.getInstance()
+            val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
+
+            context.imageLoader.enqueue(
+                ImageRequest.Builder(context)
+                    .data(url)
+                    .addHeader("Authorization", credentials)
+                    .placeholder(R.drawable.ic_user)
+                    .transformations(CircleCropTransformation())
+                    .crossfade(true)
+                    .target(target)
+                    .build()
             )
         }
     }
 
-    private fun loadUserAvatar(menuItem: MenuItem) {
-        if (activity != null) {
-            val imageRequest = DisplayUtils.getImageRequestForUrl(
-                ApiUtils.getUrlForAvatar(
-                    currentUser!!.baseUrl,
-                    currentUser!!.userId,
-                    true
-                ),
-                currentUser
-            )
-            val imagePipeline = Fresco.getImagePipeline()
-            val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
-            dataSource.subscribe(
-                object : BaseBitmapDataSubscriber() {
-                    override fun onNewResultImpl(bitmap: Bitmap?) {
-                        if (bitmap != null && resources != null) {
-                            val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(
-                                resources!!,
-                                bitmap
-                            )
-                            roundedBitmapDrawable.isCircular = true
-                            roundedBitmapDrawable.setAntiAlias(true)
-                            menuItem.icon = roundedBitmapDrawable
-                        }
-                    }
+    private fun loadUserAvatar(button: MaterialButton) {
 
-                    override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>>) {
-                        if (resources != null) {
-                            menuItem.icon = ResourcesCompat.getDrawable(resources!!, R.drawable.ic_user, null)
-                        }
-                    }
-                },
-                UiThreadImmediateExecutorService.getInstance()
-            )
+        val target = object : Target {
+            override fun onStart(placeholder: Drawable?) {
+                button.icon = placeholder
+            }
+            override fun onSuccess(result: Drawable) {
+                button.icon = result
+            }
+        }
+
+        loadUserAvatar(target)
+    }
+
+    private fun loadUserAvatar(menuItem: MenuItem) {
+        val target = object : Target {
+            override fun onStart(placeholder: Drawable?) {
+                menuItem.icon = placeholder
+            }
+            override fun onSuccess(result: Drawable) {
+                menuItem.icon = result
+            }
         }
+        loadUserAvatar(target)
     }
 
     override fun onAttach(view: View) {

+ 283 - 0
app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt

@@ -0,0 +1,283 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+@file:Suppress("TooManyFunctions")
+
+package com.nextcloud.talk.extensions
+
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Build
+import android.util.Log
+import android.widget.ImageView
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import coil.annotation.ExperimentalCoilApi
+import coil.imageLoader
+import coil.load
+import coil.request.ImageRequest
+import coil.request.SuccessResult
+import coil.result
+import coil.transform.CircleCropTransformation
+import coil.transform.RoundedCornersTransformation
+import com.amulyakhare.textdrawable.TextDrawable
+import com.nextcloud.talk.R
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.utils.ApiUtils
+
+private const val ROUNDING_PIXEL = 16f
+private const val TAG = "ImageViewExtensions"
+
+fun ImageView.loadAvatar(
+    user: User,
+    avatar: String,
+    requestBigSize: Boolean = true
+): io.reactivex.disposables
+.Disposable {
+
+    val imageRequestUri = ApiUtils.getUrlForAvatar(
+        user.baseUrl,
+        avatar,
+        requestBigSize
+    )
+
+    return loadAvatarInternal(user, imageRequestUri, false)
+}
+
+fun ImageView.replaceAvatar(
+    user: User,
+    avatar: String,
+    requestBigSize: Boolean = true
+): io.reactivex.disposables
+.Disposable {
+
+    val imageRequestUri = ApiUtils.getUrlForAvatar(
+        user.baseUrl,
+        avatar,
+        requestBigSize
+    )
+
+    return loadAvatarInternal(user, imageRequestUri, true)
+}
+
+@OptIn(ExperimentalCoilApi::class)
+private fun ImageView.loadAvatarInternal(
+    user: User?,
+    url: String,
+    replace: Boolean
+): io.reactivex.disposables
+.Disposable {
+
+    if (replace && this.result is SuccessResult) {
+        val result = this.result as SuccessResult
+        val memoryCacheKey = result.memoryCacheKey
+        val memoryCache = context.imageLoader.memoryCache
+        memoryCacheKey?.let { memoryCache?.remove(it) }
+
+        val diskCacheKey = result.diskCacheKey
+        val diskCache = context.imageLoader.diskCache
+        diskCacheKey?.let { diskCache?.remove(it) }
+    }
+
+    return DisposableWrapper(
+        load(url) {
+            user?.let {
+                addHeader(
+                    "Authorization",
+                    ApiUtils.getCredentials(user.username, user.token)
+                )
+            }
+            transformations(CircleCropTransformation())
+            placeholder(R.drawable.account_circle_96dp)
+            listener(onError = { _, result ->
+                Log.w(TAG, "Can't load avatar with URL: $url", result.throwable)
+            })
+        }
+    )
+}
+
+@Deprecated("Use function loadAvatar", level = DeprecationLevel.WARNING)
+fun ImageView.loadAvatarWithUrl(user: User? = null, url: String): io.reactivex.disposables.Disposable {
+    return loadAvatarInternal(user, url, false)
+}
+
+fun ImageView.loadThumbnail(url: String, user: User): io.reactivex.disposables.Disposable {
+    val requestBuilder = ImageRequest.Builder(context)
+        .data(url)
+        .crossfade(true)
+        .target(this)
+        .transformations(CircleCropTransformation())
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        val layers = arrayOfNulls<Drawable>(2)
+        layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
+        layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
+        requestBuilder.placeholder(LayerDrawable(layers))
+    } else {
+        requestBuilder.placeholder(R.mipmap.ic_launcher)
+    }
+
+    if (url.startsWith(user.baseUrl!!) &&
+        (url.contains("index.php/core/preview?fileId=") || url.contains("/avatar/"))
+    ) {
+        requestBuilder.addHeader(
+            "Authorization",
+            ApiUtils.getCredentials(user.username, user.token)
+        )
+    }
+
+    return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build()))
+}
+
+fun ImageView.loadImage(url: String, user: User, placeholder: Drawable? = null): io.reactivex.disposables.Disposable {
+
+    val requestBuilder = ImageRequest.Builder(context)
+        .data(url)
+        .crossfade(true)
+        .target(this)
+        .placeholder(placeholder)
+        .error(placeholder)
+        .transformations(RoundedCornersTransformation(ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL))
+
+    if (url.startsWith(user.baseUrl!!) &&
+        (url.contains("index.php/core/preview?fileId=") || url.contains("/avatar/"))
+    ) {
+        requestBuilder.addHeader(
+            "Authorization",
+            ApiUtils.getCredentials(user.username, user.token)
+        )
+    }
+
+    return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build()))
+}
+
+fun ImageView.loadAvatarOrImagePreview(url: String, user: User, placeholder: Drawable? = null): io.reactivex
+.disposables.Disposable {
+    return if (url.contains("/avatar/")) {
+        loadAvatarInternal(user, url, false)
+    } else {
+        loadImage(url, user, placeholder)
+    }
+}
+
+fun ImageView.loadAvatar(any: Any?): io.reactivex.disposables.Disposable {
+    return DisposableWrapper(
+        load(any) {
+            transformations(CircleCropTransformation())
+        }
+    )
+}
+
+fun ImageView.loadSystemAvatar(): io.reactivex.disposables.Disposable {
+    val data: Any = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        val layers = arrayOfNulls<Drawable>(2)
+        layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background)
+        layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground)
+        val layerDrawable = LayerDrawable(layers)
+        layerDrawable
+    } else {
+        R.mipmap.ic_launcher
+    }
+
+    return DisposableWrapper(
+        load(data) {
+            transformations(CircleCropTransformation())
+        }
+    )
+}
+
+fun ImageView.loadChangelogBotAvatar(): io.reactivex.disposables.Disposable {
+    return loadSystemAvatar()
+}
+
+fun ImageView.loadBotsAvatar(): io.reactivex.disposables.Disposable {
+    return loadAvatar(
+        TextDrawable.builder()
+            .beginConfig()
+            .bold()
+            .endConfig()
+            .buildRound(
+                ">",
+                ResourcesCompat.getColor(context.resources, R.color.black, null)
+            )
+    )
+}
+
+fun ImageView.loadGroupCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
+
+    val data: Any = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_group) as Any
+    } else {
+        R.drawable.ic_circular_group
+    }
+    return loadAvatar(data)
+}
+
+fun ImageView.loadPublicCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
+    val data: Any = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_link) as Any
+    } else {
+        R.drawable.ic_circular_link
+    }
+    return loadAvatar(data)
+}
+
+fun ImageView.loadMailAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable {
+    val data: Any = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_mail) as Any
+    } else {
+        R.drawable.ic_circular_mail
+    }
+    return loadAvatar(data)
+}
+
+fun ImageView.loadGuestAvatar(user: User, name: String, big: Boolean): io.reactivex.disposables.Disposable {
+    return loadGuestAvatar(user.baseUrl!!, name, big)
+}
+
+fun ImageView.loadGuestAvatar(baseUrl: String, name: String, big: Boolean): io.reactivex.disposables.Disposable {
+    val imageRequestUri = ApiUtils.getUrlForGuestAvatar(
+        baseUrl,
+        name,
+        big
+    )
+    return DisposableWrapper(
+        load(imageRequestUri) {
+            transformations(CircleCropTransformation())
+            listener(onError = { _, result ->
+                Log.w(TAG, "Can't load guest avatar with URL: $imageRequestUri", result.throwable)
+            })
+        }
+    )
+}
+
+private class DisposableWrapper(private val disposable: coil.request.Disposable) : io.reactivex.disposables
+    .Disposable {
+
+    override fun dispose() {
+        disposable.dispose()
+    }
+
+    override fun isDisposed(): Boolean {
+        return disposable.isDisposed
+    }
+}

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt

@@ -526,7 +526,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
                 notificationUser.id,
                 false
             ) else ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.name, false)
-            person.setIcon(loadAvatarSync(avatarUrl))
+            person.setIcon(loadAvatarSync(avatarUrl, context!!))
         }
         notificationBuilder.setStyle(getStyle(person.build(), style))
     }

+ 7 - 34
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt

@@ -22,16 +22,15 @@ package com.nextcloud.talk.polls.adapters
 
 import android.annotation.SuppressLint
 import android.text.TextUtils
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.interfaces.DraweeController
+import android.widget.ImageView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.PollResultVoterItemBinding
+import com.nextcloud.talk.extensions.loadAvatar
+import com.nextcloud.talk.extensions.loadGuestAvatar
 import com.nextcloud.talk.polls.model.PollDetails
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 
 class PollResultVoterViewHolder(
     private val user: User,
@@ -46,45 +45,19 @@ class PollResultVoterViewHolder(
         binding.root.setOnClickListener { clickListener.onClick() }
 
         binding.pollVoterName.text = item.details.actorDisplayName
-        binding.pollVoterAvatar.controller = getAvatarDraweeController(item.details)
+        loadAvatar(item.details, binding.pollVoterAvatar)
         viewThemeUtils.dialog.colorDialogSupportingText(binding.pollVoterName)
     }
 
-    private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? {
-        var draweeController: DraweeController? = null
+    private fun loadAvatar(pollDetail: PollDetails, avatar: ImageView) {
         if (pollDetail.actorType == "guests") {
             var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest)
             if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) {
                 displayName = pollDetail.actorDisplayName!!
             }
-            draweeController = Fresco.newDraweeControllerBuilder()
-                .setAutoPlayAnimations(true)
-                .setImageRequest(
-                    DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForGuestAvatar(
-                            user.baseUrl,
-                            displayName,
-                            false
-                        ),
-                        user
-                    )
-                )
-                .build()
+            avatar.loadGuestAvatar(user, displayName!!, false)
         } else if (pollDetail.actorType == "users") {
-            draweeController = Fresco.newDraweeControllerBuilder()
-                .setAutoPlayAnimations(true)
-                .setImageRequest(
-                    DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForAvatar(
-                            user.baseUrl,
-                            pollDetail.actorId,
-                            false
-                        ),
-                        user
-                    )
-                )
-                .build()
+            avatar.loadAvatar(user, pollDetail.actorId!!, false)
         }
-        return draweeController
     }
 }

+ 10 - 49
app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Marcel Hibbe
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -22,20 +24,16 @@ package com.nextcloud.talk.polls.adapters
 
 import android.annotation.SuppressLint
 import android.text.TextUtils
+import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
-import androidx.core.content.res.ResourcesCompat
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.generic.RoundingParams
-import com.facebook.drawee.interfaces.DraweeController
-import com.facebook.drawee.view.SimpleDraweeView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding
+import com.nextcloud.talk.extensions.loadAvatar
+import com.nextcloud.talk.extensions.loadGuestAvatar
 import com.nextcloud.talk.polls.model.PollDetails
-import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 
 class PollResultVotersOverviewViewHolder(
     private val user: User,
@@ -61,24 +59,14 @@ class PollResultVotersOverviewViewHolder(
 
         for (i in 0 until avatarsToDisplay) {
             val pollDetails = item.detailsList[i]
-            val avatar = SimpleDraweeView(binding.root.context)
+            val avatar = ImageView(binding.root.context)
 
             layoutParams.marginStart = i * AVATAR_OFFSET
             avatar.layoutParams = layoutParams
 
             avatar.translationZ = i.toFloat() * -1
 
-            val roundingParams = RoundingParams.fromCornersRadius(AVATAR_RADIUS)
-            roundingParams.roundAsCircle = true
-            roundingParams.borderColor = ResourcesCompat.getColor(
-                itemView.context.resources!!,
-                R.color.vote_dialog_background,
-                null
-            )
-            roundingParams.borderWidth = DisplayUtils.convertDpToPixel(2.0f, itemView.context)
-
-            avatar.hierarchy.roundingParams = roundingParams
-            avatar.controller = getAvatarDraweeController(pollDetails)
+            loadAvatar(pollDetails, avatar)
 
             binding.votersAvatarsOverviewWrapper.addView(avatar)
 
@@ -92,47 +80,20 @@ class PollResultVotersOverviewViewHolder(
         }
     }
 
-    private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? {
-        var draweeController: DraweeController? = null
+    private fun loadAvatar(pollDetail: PollDetails, avatar: ImageView) {
         if (pollDetail.actorType == "guests") {
             var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest)
             if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) {
                 displayName = pollDetail.actorDisplayName!!
             }
-            draweeController = Fresco.newDraweeControllerBuilder()
-                .setAutoPlayAnimations(true)
-                .setImageRequest(
-                    DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForGuestAvatar(
-                            user.baseUrl,
-                            displayName,
-                            false
-                        ),
-                        user
-                    )
-                )
-                .build()
+            avatar.loadGuestAvatar(user, displayName!!, false)
         } else if (pollDetail.actorType == "users") {
-            draweeController = Fresco.newDraweeControllerBuilder()
-                .setAutoPlayAnimations(true)
-                .setImageRequest(
-                    DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForAvatar(
-                            user.baseUrl,
-                            pollDetail.actorId,
-                            false
-                        ),
-                        user
-                    )
-                )
-                .build()
+            avatar.loadAvatar(user, pollDetail.actorId!!, false)
         }
-        return draweeController
     }
 
     companion object {
         const val AVATAR_SIZE = 60
-        const val AVATAR_RADIUS = 5f
         const val MAX_AVATARS = 10
         const val AVATAR_OFFSET = AVATAR_SIZE - 20
         const val DOTS_OFFSET = 70

+ 1 - 1
app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt

@@ -162,7 +162,7 @@ class DirectReplyReceiver : BroadcastReceiver() {
         val avatarUrl = ApiUtils.getUrlForAvatar(currentUser.baseUrl, currentUser.userId, false)
         val me = Person.Builder()
             .setName(currentUser.displayName)
-            .setIcon(NotificationUtils.loadAvatarSync(avatarUrl))
+            .setIcon(NotificationUtils.loadAvatarSync(avatarUrl, context))
             .build()
         val message = NotificationCompat.MessagingStyle.Message(reply, System.currentTimeMillis(), me)
         previousStyle?.addMessage(message)

+ 7 - 16
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt

@@ -22,20 +22,18 @@ package com.nextcloud.talk.remotefilebrowser.adapters
 
 import android.text.format.Formatter
 import android.view.View
+import android.widget.ImageView
 import autodagger.AutoInjector
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.interfaces.DraweeController
-import com.facebook.drawee.view.SimpleDraweeView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.RvItemBrowserFileBinding
+import com.nextcloud.talk.extensions.loadImage
 import com.nextcloud.talk.remotefilebrowser.SelectionInterface
 import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DateUtils.getLocalDateTimeStringFromTimestamp
-import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.Mimetype.FOLDER
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -48,7 +46,7 @@ class RemoteFileBrowserItemsListViewHolder(
     onItemClicked: (Int) -> Unit
 ) : RemoteFileBrowserItemsViewHolder(binding, mimeTypeSelectionFilter, currentUser, selectionInterface) {
 
-    override val fileIcon: SimpleDraweeView
+    override val fileIcon: ImageView
         get() = binding.fileIcon
 
     private var selectable: Boolean = true
@@ -68,7 +66,6 @@ class RemoteFileBrowserItemsListViewHolder(
     override fun onBind(item: RemoteFileBrowserItem) {
         super.onBind(item)
 
-        binding.fileIcon.controller = null
         if (!item.isAllowedToReShare || item.isEncrypted) {
             binding.root.isEnabled = false
             binding.root.alpha = DISABLED_ALPHA
@@ -95,11 +92,7 @@ class RemoteFileBrowserItemsListViewHolder(
         calculateClickability(item, selectable)
         setSelectability()
 
-        binding.fileIcon
-            .hierarchy
-            .setPlaceholderImage(
-                viewThemeUtils.talk.getPlaceholderImage(binding.root.context, item.mimeType)
-            )
+        val placeholder = viewThemeUtils.talk.getPlaceholderImage(binding.root.context, item.mimeType)
 
         if (item.hasPreview) {
             val path = ApiUtils.getUrlForFilePreviewWithRemotePath(
@@ -108,12 +101,10 @@ class RemoteFileBrowserItemsListViewHolder(
                 binding.fileIcon.context.resources.getDimensionPixelSize(R.dimen.small_item_height)
             )
             if (path.isNotEmpty()) {
-                val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
-                    .setAutoPlayAnimations(true)
-                    .setImageRequest(DisplayUtils.getImageRequestForUrl(path))
-                    .build()
-                binding.fileIcon.controller = draweeController
+                binding.fileIcon.loadImage(path, currentUser, placeholder)
             }
+        } else {
+            binding.fileIcon.setImageDrawable(placeholder)
         }
 
         binding.filenameTextView.text = item.displayName

+ 3 - 13
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt

@@ -20,11 +20,9 @@
 
 package com.nextcloud.talk.remotefilebrowser.adapters
 
-import android.graphics.drawable.Drawable
-import androidx.core.content.ContextCompat
+import android.widget.ImageView
 import androidx.recyclerview.widget.RecyclerView
 import androidx.viewbinding.ViewBinding
-import com.facebook.drawee.view.SimpleDraweeView
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.remotefilebrowser.SelectionInterface
 import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
@@ -37,17 +35,9 @@ abstract class RemoteFileBrowserItemsViewHolder(
     val selectionInterface: SelectionInterface,
 ) : RecyclerView.ViewHolder(binding.root) {
 
-    abstract val fileIcon: SimpleDraweeView
+    abstract val fileIcon: ImageView
 
     open fun onBind(item: RemoteFileBrowserItem) {
-        fileIcon.hierarchy.setPlaceholderImage(staticImage(item.mimeType, fileIcon))
-    }
-
-    private fun staticImage(
-        mimeType: String?,
-        image: SimpleDraweeView
-    ): Drawable {
-        val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimeType)
-        return ContextCompat.getDrawable(image.context, drawableResourceId)!!
+        fileIcon.setImageResource(DrawableUtils.getDrawableResourceIdForMimeType(item.mimeType))
     }
 }

+ 2 - 2
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt

@@ -23,8 +23,8 @@
 package com.nextcloud.talk.shareditems.adapters
 
 import android.view.View
+import android.widget.ImageView
 import android.widget.ProgressBar
-import com.facebook.drawee.view.SimpleDraweeView
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.SharedItemGridBinding
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
@@ -35,7 +35,7 @@ class SharedItemsGridViewHolder(
     viewThemeUtils: ViewThemeUtils
 ) : SharedItemsViewHolder(binding, user, viewThemeUtils) {
 
-    override val image: SimpleDraweeView
+    override val image: ImageView
         get() = binding.image
     override val clickTarget: View
         get() = binding.image

+ 7 - 6
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt

@@ -26,9 +26,10 @@ import android.content.Context
 import android.content.Intent
 import android.text.format.Formatter
 import android.view.View
+import android.widget.ImageView
 import android.widget.ProgressBar
 import androidx.core.content.ContextCompat
-import com.facebook.drawee.view.SimpleDraweeView
+import coil.load
 import com.nextcloud.talk.R
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.SharedItemListBinding
@@ -46,7 +47,7 @@ class SharedItemsListViewHolder(
     viewThemeUtils: ViewThemeUtils
 ) : SharedItemsViewHolder(binding, user, viewThemeUtils) {
 
-    override val image: SimpleDraweeView
+    override val image: ImageView
         get() = binding.fileImage
     override val clickTarget: View
         get() = binding.fileItem
@@ -75,7 +76,7 @@ class SharedItemsListViewHolder(
         binding.separator1.visibility = View.GONE
         binding.fileDate.text = item.dateTime
         binding.actor.text = item.actorName
-        image.hierarchy.setPlaceholderImage(R.drawable.ic_baseline_bar_chart_24)
+        image.load(R.drawable.ic_baseline_bar_chart_24)
         image.setColorFilter(
             ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon),
             android.graphics.PorterDuff.Mode.SRC_IN
@@ -93,7 +94,7 @@ class SharedItemsListViewHolder(
         binding.separator1.visibility = View.GONE
         binding.fileDate.text = item.dateTime
         binding.actor.text = item.actorName
-        image.hierarchy.setPlaceholderImage(R.drawable.ic_baseline_location_on_24)
+        image.load(R.drawable.ic_baseline_location_on_24)
         image.setColorFilter(
             ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon),
             android.graphics.PorterDuff.Mode.SRC_IN
@@ -114,7 +115,7 @@ class SharedItemsListViewHolder(
         binding.separator1.visibility = View.GONE
         binding.fileDate.text = item.dateTime
         binding.actor.text = item.actorName
-        image.hierarchy.setPlaceholderImage(R.drawable.ic_mimetype_file)
+        image.load(R.drawable.ic_mimetype_file)
         image.setColorFilter(
             ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon),
             android.graphics.PorterDuff.Mode.SRC_IN
@@ -129,7 +130,7 @@ class SharedItemsListViewHolder(
         binding.separator1.visibility = View.GONE
         binding.fileDate.text = item.dateTime
         binding.actor.text = item.actorName
-        image.hierarchy.setPlaceholderImage(R.drawable.ic_baseline_deck_24)
+        image.load(R.drawable.ic_baseline_deck_24)
         image.setColorFilter(
             ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon),
             android.graphics.PorterDuff.Mode.SRC_IN

+ 13 - 44
app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt

@@ -23,29 +23,20 @@
 package com.nextcloud.talk.shareditems.adapters
 
 import android.content.Context
-import android.net.Uri
-import android.util.Log
 import android.view.View
+import android.widget.ImageView
 import android.widget.ProgressBar
 import androidx.recyclerview.widget.RecyclerView
 import androidx.viewbinding.ViewBinding
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.controller.BaseControllerListener
-import com.facebook.drawee.controller.ControllerListener
-import com.facebook.drawee.interfaces.DraweeController
-import com.facebook.drawee.view.SimpleDraweeView
-import com.facebook.imagepipeline.common.RotationOptions
-import com.facebook.imagepipeline.image.ImageInfo
-import com.facebook.imagepipeline.request.ImageRequestBuilder
 import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.extensions.loadImage
 import com.nextcloud.talk.shareditems.model.SharedDeckCardItem
 import com.nextcloud.talk.shareditems.model.SharedFileItem
 import com.nextcloud.talk.shareditems.model.SharedItem
-import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.shareditems.model.SharedLocationItem
 import com.nextcloud.talk.shareditems.model.SharedOtherItem
 import com.nextcloud.talk.shareditems.model.SharedPollItem
-import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.FileViewerUtils
 
 abstract class SharedItemsViewHolder(
@@ -58,21 +49,21 @@ abstract class SharedItemsViewHolder(
         private val TAG = SharedItemsViewHolder::class.simpleName
     }
 
-    abstract val image: SimpleDraweeView
+    abstract val image: ImageView
     abstract val clickTarget: View
     abstract val progressBar: ProgressBar
 
-    private val authHeader = mapOf(
-        Pair(
-            "Authorization",
-            ApiUtils.getCredentials(user.username, user.token)
-        )
-    )
-
     open fun onBind(item: SharedFileItem) {
-        image.hierarchy.setPlaceholderImage(viewThemeUtils.talk.getPlaceholderImage(image.context, item.mimeType))
+
+        val placeholder = viewThemeUtils.talk.getPlaceholderImage(image.context, item.mimeType)
         if (item.previewAvailable) {
-            image.controller = configurePreview(item)
+            image.loadImage(
+                item.previewLink,
+                user,
+                placeholder
+            )
+        } else {
+            image.setImageDrawable(placeholder)
         }
 
         /*
@@ -105,28 +96,6 @@ abstract class SharedItemsViewHolder(
         )
     }
 
-    private fun configurePreview(item: SharedFileItem): DraweeController {
-        val imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(item.previewLink))
-            .setProgressiveRenderingEnabled(true)
-            .setRotationOptions(RotationOptions.autoRotate())
-            .disableDiskCache()
-            .setHeaders(authHeader)
-            .build()
-
-        val listener: ControllerListener<ImageInfo?> = object : BaseControllerListener<ImageInfo?>() {
-            override fun onFailure(id: String, e: Throwable) {
-                Log.w(TAG, "Failed to load image. A static mimetype image will be used", e)
-            }
-        }
-
-        return Fresco.newDraweeControllerBuilder()
-            .setOldController(image.controller)
-            .setAutoPlayAnimations(true)
-            .setImageRequest(imageRequest)
-            .setControllerListener(listener)
-            .build()
-    }
-
     open fun onBind(item: SharedPollItem, showPoll: (item: SharedItem, context: Context) -> Unit) {}
 
     open fun onBind(item: SharedLocationItem) {}

+ 2 - 15
app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java

@@ -33,8 +33,6 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.interfaces.DraweeController;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.nextcloud.talk.activities.MainActivity;
 import com.nextcloud.talk.adapters.items.AdvancedUserItem;
@@ -42,6 +40,7 @@ import com.nextcloud.talk.api.NcApi;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.DialogChooseAccountBinding;
+import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.models.json.status.Status;
 import com.nextcloud.talk.models.json.status.StatusOverall;
@@ -132,22 +131,10 @@ public class ChooseAccountDialogFragment extends DialogFragment {
             if (user.getBaseUrl() != null &&
                 (user.getBaseUrl().startsWith("http://") || user.getBaseUrl().startsWith("https://"))) {
                 binding.currentAccount.userIcon.setVisibility(View.VISIBLE);
-
-                DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(binding.currentAccount.userIcon.getController())
-                    .setAutoPlayAnimations(true)
-                    .setImageRequest(DisplayUtils.getImageRequestForUrl(
-                        ApiUtils.getUrlForAvatar(
-                            user.getBaseUrl(),
-                            user.getUserId(),
-                            false)))
-                    .build();
-                binding.currentAccount.userIcon.setController(draweeController);
-
+                ImageViewExtensionsKt.loadAvatar(binding.currentAccount.userIcon, user, user.getUserId(), true);
             } else {
                 binding.currentAccount.userIcon.setVisibility(View.INVISIBLE);
             }
-
             loadCurrentStatus(user);
         }
     }

+ 2 - 19
app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt

@@ -33,8 +33,6 @@ import android.view.ViewGroup
 import androidx.fragment.app.DialogFragment
 import androidx.recyclerview.widget.LinearLayoutManager
 import autodagger.AutoInjector
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.interfaces.DraweeController
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.nextcloud.talk.activities.MainActivity
 import com.nextcloud.talk.adapters.items.AdvancedUserItem
@@ -42,11 +40,10 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.DialogChooseAccountShareToBinding
+import com.nextcloud.talk.extensions.loadAvatar
 import com.nextcloud.talk.models.json.participants.Participant
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.users.UserManager
-import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.DisplayUtils
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
 import java.net.CookieManager
@@ -97,21 +94,7 @@ class ChooseAccountShareToDialogFragment : DialogFragment() {
             if (user.baseUrl != null &&
                 (user.baseUrl!!.startsWith("http://") || user.baseUrl!!.startsWith("https://"))
             ) {
-                binding!!.currentAccount.userIcon.visibility = View.VISIBLE
-                val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(binding!!.currentAccount.userIcon.controller)
-                    .setAutoPlayAnimations(true)
-                    .setImageRequest(
-                        DisplayUtils.getImageRequestForUrl(
-                            ApiUtils.getUrlForAvatar(
-                                user.baseUrl,
-                                user.userId,
-                                false
-                            )
-                        )
-                    )
-                    .build()
-                binding!!.currentAccount.userIcon.controller = draweeController
+                binding!!.currentAccount.userIcon.loadAvatar(user, user.userId!!)
             } else {
                 binding!!.currentAccount.userIcon.visibility = View.INVISIBLE
             }

+ 39 - 171
app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java

@@ -3,6 +3,8 @@
  *
  * @author Mario Danic
  * @author Andy Scherzinger
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
  *
@@ -33,11 +35,7 @@ import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Typeface;
-import android.graphics.drawable.Animatable;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.VectorDrawable;
 import android.net.Uri;
 import android.os.Build;
 import android.text.Spannable;
@@ -51,36 +49,19 @@ import android.text.style.AbsoluteSizeSpan;
 import android.text.style.ClickableSpan;
 import android.text.style.ForegroundColorSpan;
 import android.text.style.StyleSpan;
-import android.util.Log;
 import android.util.TypedValue;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.Window;
 import android.widget.EditText;
+import android.widget.ImageView;
 import android.widget.TextView;
 
-import com.facebook.common.executors.UiThreadImmediateExecutorService;
-import com.facebook.common.references.CloseableReference;
-import com.facebook.datasource.DataSource;
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.controller.ControllerListener;
-import com.facebook.drawee.interfaces.DraweeController;
-import com.facebook.drawee.view.SimpleDraweeView;
-import com.facebook.imagepipeline.common.RotationOptions;
-import com.facebook.imagepipeline.core.ImagePipeline;
-import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
-import com.facebook.imagepipeline.image.CloseableImage;
-import com.facebook.imagepipeline.image.ImageInfo;
-import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor;
-import com.facebook.imagepipeline.postprocessors.RoundPostprocessor;
-import com.facebook.imagepipeline.request.ImageRequest;
-import com.facebook.imagepipeline.request.ImageRequestBuilder;
-import com.facebook.widget.text.span.BetterImageSpan;
 import com.google.android.material.chip.ChipDrawable;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.events.UserMentionClickEvent;
+import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
 import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.utils.text.Spans;
 
@@ -91,8 +72,6 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.text.DateFormat;
 import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -109,6 +88,11 @@ import androidx.core.content.res.ResourcesCompat;
 import androidx.core.graphics.ColorUtils;
 import androidx.core.graphics.drawable.DrawableCompat;
 import androidx.emoji.text.EmojiCompat;
+import coil.Coil;
+import coil.request.ImageRequest;
+import coil.target.Target;
+import coil.transform.CircleCropTransformation;
+import third.parties.fresco.BetterImageSpan;
 
 import static com.nextcloud.talk.utils.FileSortOrder.sort_a_to_z_id;
 import static com.nextcloud.talk.utils.FileSortOrder.sort_big_to_small_id;
@@ -119,8 +103,6 @@ import static com.nextcloud.talk.utils.FileSortOrder.sort_z_to_a_id;
 
 public class DisplayUtils {
 
-    private static final String TAG = "DisplayUtils";
-
     private static final int INDEX_LUMINATION = 2;
     private static final double MAX_LIGHTNESS = 0.92;
 
@@ -154,33 +136,6 @@ public class DisplayUtils {
         textView.setMovementMethod(LinkMovementMethod.getInstance());
     }
 
-    private static void updateViewSize(@Nullable ImageInfo imageInfo, SimpleDraweeView draweeView) {
-        if (imageInfo != null && draweeView.getId() != R.id.messageUserAvatar) {
-            int maxSize = draweeView.getContext().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size);
-            draweeView.getLayoutParams().width = imageInfo.getWidth() > maxSize ? maxSize : imageInfo.getWidth();
-            draweeView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
-            draweeView.setAspectRatio((float) imageInfo.getWidth() / imageInfo.getHeight());
-            draweeView.requestLayout();
-        }
-    }
-
-    public static Drawable getRoundedDrawable(Drawable drawable) {
-        Bitmap bitmap = getBitmap(drawable);
-        new RoundAsCirclePostprocessor(true).process(bitmap);
-        return new BitmapDrawable(bitmap);
-    }
-
-    public static Bitmap getRoundedBitmapFromVectorDrawableResource(Resources resources, int resource) {
-        VectorDrawable vectorDrawable = (VectorDrawable) ResourcesCompat.getDrawable(resources, resource, null);
-        Bitmap bitmap = getBitmap(vectorDrawable);
-        new RoundPostprocessor(true).process(bitmap);
-        return bitmap;
-    }
-
-    public static Drawable getRoundedBitmapDrawableFromVectorDrawableResource(Resources resources, int resource) {
-        return new BitmapDrawable(getRoundedBitmapFromVectorDrawableResource(resources, resource));
-    }
-
     public static Bitmap getBitmap(Drawable drawable) {
         Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                                             drawable.getIntrinsicHeight(),
@@ -191,60 +146,6 @@ public class DisplayUtils {
         return bitmap;
     }
 
-    public static ImageRequest getImageRequestForUrl(String url) {
-        return getImageRequestForUrl(url, (User) null);
-    }
-
-    public static ImageRequest getImageRequestForUrl(String url, @Nullable User user) {
-        Map<String, String> headers = new HashMap<>();
-        if (user != null &&
-            url.startsWith(user.getBaseUrl()) &&
-            (url.contains("index.php/core/preview?fileId=") || url.contains("/avatar/"))) {
-            headers.put("Authorization", ApiUtils.getCredentials(user.getUsername(), user.getToken()));
-        }
-
-        return ImageRequestBuilder.newBuilderWithSource(Uri.parse(url))
-            .setProgressiveRenderingEnabled(true)
-            .setRotationOptions(RotationOptions.autoRotate())
-            .disableDiskCache()
-            .setHeaders(headers)
-            .build();
-    }
-
-    public static ControllerListener getImageControllerListener(SimpleDraweeView draweeView) {
-        return new ControllerListener() {
-            @Override
-            public void onSubmit(String id, Object callerContext) {
-                // unused atm
-            }
-
-            @Override
-            public void onFinalImageSet(String id, @Nullable Object imageInfo, @Nullable Animatable animatable) {
-                updateViewSize((ImageInfo) imageInfo, draweeView);
-            }
-
-            @Override
-            public void onIntermediateImageSet(String id, @Nullable Object imageInfo) {
-                updateViewSize((ImageInfo) imageInfo, draweeView);
-            }
-
-            @Override
-            public void onIntermediateImageFailed(String id, Throwable throwable) {
-                // unused atm
-            }
-
-            @Override
-            public void onFailure(String id, Throwable throwable) {
-                // unused atm
-            }
-
-            @Override
-            public void onRelease(String id) {
-                // unused atm
-            }
-        };
-    }
-
     public static float convertDpToPixel(float dp, Context context) {
         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                                                     context.getResources().getDisplayMetrics()) + 0.5f);
@@ -335,33 +236,37 @@ public class DisplayUtils {
                     conversationUser.getBaseUrl(),
                     String.valueOf(label), true);
             }
-            ImageRequest imageRequest = getImageRequestForUrl(url);
-            ImagePipeline imagePipeline = Fresco.getImagePipeline();
-            DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(
-                imageRequest,
-                context);
-
-            dataSource.subscribe(
-                new BaseBitmapDataSubscriber() {
+
+            ImageRequest imageRequest = new ImageRequest.Builder(context)
+                .data(url)
+                .crossfade(true)
+                .transformations(new CircleCropTransformation())
+                .target(new Target() {
                     @Override
-                    protected void onNewResultImpl(Bitmap bitmap) {
-                        if (bitmap != null) {
-                            chip.setChipIcon(getRoundedDrawable(new BitmapDrawable(bitmap)));
-
-                            // A hack to refresh the chip icon
-                            if (emojiEditText != null) {
-                                emojiEditText.post(() -> emojiEditText.setTextKeepState(
-                                    emojiEditText.getText(),
-                                    TextView.BufferType.SPANNABLE));
-                            }
-                        }
+                    public void onStart(@Nullable Drawable drawable) {
+
                     }
 
                     @Override
-                    protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
+                    public void onError(@Nullable Drawable drawable) {
+
                     }
-                },
-                UiThreadImmediateExecutorService.getInstance());
+
+                    @Override
+                    public void onSuccess(@NonNull Drawable drawable) {
+                        chip.setChipIcon(drawable);
+
+                        // A hack to refresh the chip icon
+                        if (emojiEditText != null) {
+                            emojiEditText.post(() -> emojiEditText.setTextKeepState(
+                                emojiEditText.getText(),
+                                TextView.BufferType.SPANNABLE));
+                        }
+                    }
+                })
+                .build();
+
+            Coil.imageLoader(context).enqueue(imageRequest);
         }
 
         return chip;
@@ -575,7 +480,7 @@ public class DisplayUtils {
         }
     }
 
-    public static void loadAvatarImage(User user, SimpleDraweeView avatarImageView, boolean deleteCache) {
+    public static void loadAvatarImage(User user, ImageView avatarImageView, boolean deleteCache) {
         String avatarId;
         if (!TextUtils.isEmpty(user.getUserId())) {
             avatarId = user.getUserId();
@@ -583,50 +488,13 @@ public class DisplayUtils {
             avatarId = user.getUsername();
         }
 
-        String avatarString = ApiUtils.getUrlForAvatar(user.getBaseUrl(), avatarId, true);
-
-        // clear cache
         if (deleteCache) {
-            Uri avatarUri = Uri.parse(avatarString);
-
-            ImagePipeline imagePipeline = Fresco.getImagePipeline();
-            imagePipeline.evictFromMemoryCache(avatarUri);
-            imagePipeline.evictFromDiskCache(avatarUri);
-            imagePipeline.evictFromCache(avatarUri);
-        }
-
-        DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-            .setOldController(avatarImageView.getController())
-            .setAutoPlayAnimations(true)
-            .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarString))
-            .build();
-        avatarImageView.setController(draweeController);
-    }
-
-    public static void loadAvatarPlaceholder(final SimpleDraweeView targetView) {
-        final Context context = targetView.getContext();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            Drawable[] layers = new Drawable[2];
-            layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
-            layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground);
-            LayerDrawable layerDrawable = new LayerDrawable(layers);
-
-            targetView.getHierarchy().setPlaceholderImage(
-                DisplayUtils.getRoundedDrawable(layerDrawable));
+            ImageViewExtensionsKt.replaceAvatar(avatarImageView, user, avatarId, true);
         } else {
-            targetView.getHierarchy().setPlaceholderImage(R.mipmap.ic_launcher);
+            ImageViewExtensionsKt.loadAvatar(avatarImageView, user, avatarId, true);
         }
     }
 
-    public static void loadImage(final SimpleDraweeView targetView, final ImageRequest imageRequest) {
-        final DraweeController newController = Fresco.newDraweeControllerBuilder()
-            .setOldController(targetView.getController())
-            .setAutoPlayAnimations(true)
-            .setImageRequest(imageRequest)
-            .build();
-        targetView.setController(newController);
-    }
-
     public static @StringRes
     int getSortOrderStringId(FileSortOrder sortOrder) {
         switch (sortOrder.getName()) {
@@ -649,7 +517,7 @@ public class DisplayUtils {
     /**
      * calculates the relative time string based on the given modification timestamp.
      *
-     * @param context the app's context
+     * @param context               the app's context
      * @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds.
      * @return a relative time string
      */

+ 3 - 2
app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt

@@ -22,7 +22,7 @@
 package com.nextcloud.talk.utils
 
 import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
-import third_parties.daveKoeller.AlphanumComparator
+import third.parties.daveKoeller.AlphanumComparator
 import java.util.Collections
 
 class FileSortOrderByName internal constructor(name: String, ascending: Boolean) : FileSortOrder(name, ascending) {
@@ -40,7 +40,8 @@ class FileSortOrderByName internal constructor(name: String, ascending: Boolean)
      * Comparator for RemoteFileBrowserItems, sorts by name.
      */
     class RemoteFileBrowserItemNameComparator(private val multiplier: Int) : Comparator<RemoteFileBrowserItem> {
-        private val alphanumComparator = AlphanumComparator<RemoteFileBrowserItem>()
+        private val alphanumComparator =
+            AlphanumComparator<RemoteFileBrowserItem>()
 
         override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int {
             return if (!left.isFile && !right.isFile) {

+ 2 - 2
app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt

@@ -28,6 +28,7 @@ import android.net.Uri
 import android.os.Build
 import android.util.Log
 import android.view.View
+import android.widget.ImageView
 import android.widget.ProgressBar
 import android.widget.Toast
 import androidx.core.content.FileProvider
@@ -36,7 +37,6 @@ import androidx.work.Data
 import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkInfo
 import androidx.work.WorkManager
-import com.facebook.drawee.view.SimpleDraweeView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.activities.FullScreenImageActivity
 import com.nextcloud.talk.activities.FullScreenMediaActivity
@@ -412,7 +412,7 @@ class FileViewerUtils(private val context: Context, private val user: User) {
     data class ProgressUi(
         val progressBar: ProgressBar?,
         val messageText: EmojiTextView?,
-        val previewImage: SimpleDraweeView
+        val previewImage: ImageView
     )
 
     data class FileInfo(

+ 32 - 26
app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt

@@ -27,18 +27,19 @@ import android.app.Notification
 import android.app.NotificationChannel
 import android.app.NotificationManager
 import android.content.Context
+import android.graphics.drawable.BitmapDrawable
 import android.media.AudioAttributes
 import android.net.Uri
 import android.os.Build
 import android.service.notification.StatusBarNotification
 import android.text.TextUtils
+import android.util.Log
 import androidx.core.graphics.drawable.IconCompat
+import coil.executeBlocking
+import coil.imageLoader
+import coil.request.ImageRequest
+import coil.transform.CircleCropTransformation
 import com.bluelinelabs.logansquare.LoganSquare
-import com.facebook.common.references.CloseableReference
-import com.facebook.datasource.DataSources
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.imagepipeline.image.CloseableBitmap
-import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor
 import com.nextcloud.talk.BuildConfig
 import com.nextcloud.talk.R
 import com.nextcloud.talk.data.user.model.User
@@ -50,6 +51,8 @@ import java.io.IOException
 @Suppress("TooManyFunctions")
 object NotificationUtils {
 
+    const val TAG = "NotificationUtils"
+
     enum class NotificationChannels {
         NOTIFICATION_CHANNEL_MESSAGES_V4,
         NOTIFICATION_CHANNEL_CALLS_V4,
@@ -320,28 +323,31 @@ object NotificationUtils {
         )
     }
 
-    /*
-    * Load user avatar synchronously.
-    * Inspired by:
-    * https://frescolib.org/docs/using-image-pipeline.html
-    * https://github.com/facebook/fresco/issues/830
-    * https://localcoder.org/using-facebooks-fresco-to-load-a-bitmap
-    */
-    fun loadAvatarSync(avatarUrl: String): IconCompat? {
-        // TODO - how to handle errors here?
+    fun loadAvatarSync(avatarUrl: String, context: Context): IconCompat? {
         var avatarIcon: IconCompat? = null
-        val imageRequest = DisplayUtils.getImageRequestForUrl(avatarUrl)
-        val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, null)
-        val closeableImageRef = DataSources.waitForFinalResult(dataSource) as CloseableReference<CloseableBitmap>?
-        val bitmap = closeableImageRef?.get()?.underlyingBitmap
-        if (bitmap != null) {
-            // According to Fresco documentation a copy of the bitmap should be made before closing the references.
-            // However, it seems to work without making a copy... ;-)
-            RoundAsCirclePostprocessor(true).process(bitmap)
-            avatarIcon = IconCompat.createWithBitmap(bitmap)
-        }
-        CloseableReference.closeSafely(closeableImageRef)
-        dataSource.close()
+
+        val request = ImageRequest.Builder(context)
+            .data(avatarUrl)
+            .transformations(CircleCropTransformation())
+            .placeholder(R.drawable.account_circle_96dp)
+            .placeholder(R.drawable.account_circle_96dp)
+            .target(
+                onSuccess = { result ->
+                    val bitmap = (result as BitmapDrawable).bitmap
+                    avatarIcon = IconCompat.createWithBitmap(bitmap)
+                },
+                onError = { error ->
+                    error?.let {
+                        val bitmap = (error as BitmapDrawable).bitmap
+                        avatarIcon = IconCompat.createWithBitmap(bitmap)
+                    }
+                    Log.w(TAG, "Can't load avatar for URL: $avatarUrl")
+                }
+            )
+            .build()
+
+        context.imageLoader.executeBlocking(request)
+
         return avatarIcon
     }
 

+ 0 - 41
app/src/main/java/com/nextcloud/talk/utils/OkHttpNetworkFetcherWithCache.java

@@ -1,41 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.utils;
-
-import com.facebook.imagepipeline.backends.okhttp3.OkHttpNetworkFetcher;
-import okhttp3.Call;
-import okhttp3.OkHttpClient;
-
-import java.util.concurrent.Executor;
-
-public class OkHttpNetworkFetcherWithCache extends OkHttpNetworkFetcher {
-    public OkHttpNetworkFetcherWithCache(OkHttpClient okHttpClient) {
-        super(okHttpClient);
-    }
-
-    public OkHttpNetworkFetcherWithCache(Call.Factory callFactory, Executor cancellationExecutor) {
-        super(callFactory, cancellationExecutor);
-    }
-
-    public OkHttpNetworkFetcherWithCache(Call.Factory callFactory, Executor cancellationExecutor, boolean disableOkHttpCache) {
-        super(callFactory, cancellationExecutor, true);
-    }
-}

+ 2 - 1
app/src/main/java/com/nextcloud/talk/utils/text/Spans.java

@@ -22,9 +22,10 @@ package com.nextcloud.talk.utils.text;
 
 import android.graphics.drawable.Drawable;
 
-import com.facebook.widget.text.span.BetterImageSpan;
+
 
 import androidx.annotation.NonNull;
+import third.parties.fresco.BetterImageSpan;
 
 public class Spans {
 

+ 1 - 1
app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java → app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java

@@ -22,7 +22,7 @@
  *
  */
 
-package third_parties.daveKoeller;
+package third.parties.daveKoeller;
 
 import java.io.Serializable;
 import java.math.BigInteger;

+ 0 - 0
app/src/main/java/third_parties/daveKoeller/lgpl-2.1.txt → app/src/main/java/third/parties/daveKoeller/lgpl-2.1.txt


+ 138 - 0
app/src/main/java/third/parties/fresco/BetterImageSpan.kt

@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2015-present, Facebook, Inc.
+ *
+ * MIT License
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package third.parties.fresco
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Paint.FontMetricsInt
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.text.style.ReplacementSpan
+import androidx.annotation.IntDef
+
+/**
+ * A better implementation of image spans that also supports centering images against the text.
+ *
+ * In order to migrate from ImageSpan, replace `new ImageSpan(drawable, alignment)` with
+ * `new BetterImageSpan(drawable, BetterImageSpan.normalizeAlignment(alignment))`.
+ *
+ * There are 2 main differences between BetterImageSpan and ImageSpan:
+ * 1. Pass in ALIGN_CENTER to center images against the text.
+ * 2. ALIGN_BOTTOM no longer unnecessarily increases the size of the text:
+ * DynamicDrawableSpan (ImageSpan's parent) adjusts sizes as if alignment was ALIGN_BASELINE
+ * which can lead to unnecessary whitespace.
+ */
+open class BetterImageSpan @JvmOverloads constructor(
+    val drawable: Drawable,
+    @param:BetterImageSpanAlignment private val mAlignment: Int = ALIGN_BASELINE
+) : ReplacementSpan() {
+    @IntDef(*[ALIGN_BASELINE, ALIGN_BOTTOM, ALIGN_CENTER])
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class BetterImageSpanAlignment
+
+    private var mWidth = 0
+    private var mHeight = 0
+    private var mBounds: Rect? = null
+    private val mFontMetricsInt = FontMetricsInt()
+
+    init {
+        updateBounds()
+    }
+
+    /**
+     * Returns the width of the image span and increases the height if font metrics are available.
+     */
+    override fun getSize(
+        paint: Paint,
+        text: CharSequence,
+        start: Int,
+        end: Int,
+        fontMetrics: FontMetricsInt?
+    ): Int {
+        updateBounds()
+        if (fontMetrics == null) {
+            return mWidth
+        }
+        val offsetAbove = getOffsetAboveBaseline(fontMetrics)
+        val offsetBelow = mHeight + offsetAbove
+        if (offsetAbove < fontMetrics.ascent) {
+            fontMetrics.ascent = offsetAbove
+        }
+        if (offsetAbove < fontMetrics.top) {
+            fontMetrics.top = offsetAbove
+        }
+        if (offsetBelow > fontMetrics.descent) {
+            fontMetrics.descent = offsetBelow
+        }
+        if (offsetBelow > fontMetrics.bottom) {
+            fontMetrics.bottom = offsetBelow
+        }
+        return mWidth
+    }
+
+    override fun draw(
+        canvas: Canvas,
+        text: CharSequence,
+        start: Int,
+        end: Int,
+        x: Float,
+        top: Int,
+        y: Int,
+        bottom: Int,
+        paint: Paint
+    ) {
+        paint.getFontMetricsInt(mFontMetricsInt)
+        val iconTop = y + getOffsetAboveBaseline(mFontMetricsInt)
+        canvas.translate(x, iconTop.toFloat())
+        drawable.draw(canvas)
+        canvas.translate(-x, -iconTop.toFloat())
+    }
+
+    private fun updateBounds() {
+        mBounds = drawable.bounds
+        mWidth = mBounds!!.width()
+        mHeight = mBounds!!.height()
+    }
+
+    private fun getOffsetAboveBaseline(fm: FontMetricsInt): Int {
+        return when (mAlignment) {
+            ALIGN_BOTTOM -> fm.descent - mHeight
+            ALIGN_CENTER -> {
+                val textHeight = fm.descent - fm.ascent
+                val offset = (textHeight - mHeight) / 2
+                fm.ascent + offset
+            }
+            ALIGN_BASELINE -> -mHeight
+            else -> -mHeight
+        }
+    }
+
+    companion object {
+        const val ALIGN_BOTTOM = 0
+        const val ALIGN_BASELINE = 1
+        const val ALIGN_CENTER = 2
+    }
+}

+ 5 - 0
app/src/main/res/drawable/shape_oval.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="#000000" />
+</shape>

+ 2 - 6
app/src/main/res/layout/account_item.xml

@@ -21,7 +21,6 @@
 -->
 <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:fresco="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="72dp"
@@ -44,7 +43,7 @@
             android:layout_alignParentStart="true"
             android:layout_centerVertical="true">
 
-            <com.facebook.drawee.view.SimpleDraweeView
+            <ImageView
                 android:id="@+id/user_icon"
                 android:layout_width="@dimen/small_item_height"
                 android:layout_height="@dimen/small_item_height"
@@ -54,10 +53,7 @@
                 android:layout_marginEnd="1dp"
                 android:layout_marginBottom="1dp"
                 android:contentDescription="@string/avatar"
-                android:src="@drawable/account_circle_48dp"
-                fresco:placeholderImage="@drawable/account_circle_48dp"
-                fresco:failureImage="@drawable/account_circle_48dp"
-                app:roundAsCircle="true"/>
+                android:src="@drawable/account_circle_48dp" />
         </FrameLayout>
 
 

+ 2 - 1
app/src/main/res/layout/activity_main.xml

@@ -113,7 +113,8 @@
                         android:transitionName="userAvatar.transitionTag"
                         app:cornerRadius="@dimen/button_corner_radius"
                         app:iconSize="@dimen/avatar_size_app_bar"
-                        tools:visibility="gone" />
+                        app:iconTint="@null"
+                        tools:icon="@drawable/ic_user" />
 
                 </FrameLayout>
 

+ 62 - 55
app/src/main/res/layout/call_activity.xml

@@ -2,9 +2,11 @@
   ~ Nextcloud Talk application
   ~
   ~ @author Mario Danic
-  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
   ~ @author Marcel Hibbe
+  ~ @author Tim Krüger
+  ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
   ~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
   ~
   ~ This program is free software: you can redistribute it and/or modify
   ~ it under the terms of the GNU General Public License as published by
@@ -69,14 +71,14 @@
                     android:visibility="invisible"
                     tools:visibility="visible" />
 
-                <com.facebook.drawee.view.SimpleDraweeView
+                <ImageView
                     android:id="@+id/switchSelfVideoButton"
                     android:layout_width="40dp"
                     android:layout_height="40dp"
                     android:layout_gravity="center_horizontal|bottom"
                     android:layout_marginBottom="20dp"
-                    app:placeholderImage="@drawable/ic_switch_video_white_24px"
-                    app:roundAsCircle="true" />
+                    app:srcCompat="@drawable/ic_switch_video_white_24px"
+                    android:contentDescription="@string/nc_call_button_content_description_switch_to_self_vide"/>
 
                 <ProgressBar
                     android:id="@+id/selfVideoViewProgressBar"
@@ -144,6 +146,7 @@
         android:id="@+id/callControls"
         android:layout_width="match_parent"
         android:layout_height="@dimen/call_controls_height"
+        android:paddingHorizontal="@dimen/call_controls_padding_horizontal"
         android:layout_alignBottom="@id/linearWrapperLayout"
         android:animateLayoutChanges="true"
         android:background="@android:color/transparent"
@@ -151,94 +154,98 @@
         android:orientation="horizontal"
         android:weightSum="5">
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageButton
             android:id="@+id/pictureInPictureButton"
             android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_marginStart="20dp"
-            android:layout_marginEnd="10dp"
-            android:elevation="10dp"
-            app:backgroundImage="@color/call_buttons_background"
-            app:placeholderImage="@drawable/ic_baseline_picture_in_picture_alt_24"
-            app:roundAsCircle="true"
-            android:layout_weight="1"/>
-
-        <com.facebook.drawee.view.SimpleDraweeView
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:layout_marginHorizontal="@dimen/call_controls_margin_horizontal"
+            android:layout_weight="1"
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/call_buttons_background"
+            app:srcCompat="@drawable/ic_baseline_picture_in_picture_alt_24"
+            android:contentDescription="@string/nc_call_button_content_description_pip" />
+
+        <ImageButton
             android:id="@+id/audioOutputButton"
             android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_marginStart="10dp"
-            android:layout_marginEnd="10dp"
-            app:backgroundImage="@color/call_buttons_background"
-            app:placeholderImage="@drawable/ic_volume_mute_white_24dp"
-            app:roundAsCircle="true"
-            android:layout_weight="1"/>
-
-        <com.facebook.drawee.view.SimpleDraweeView
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:layout_marginHorizontal="@dimen/call_controls_margin_horizontal"
+            android:layout_weight="1"
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/call_buttons_background"
+            app:srcCompat="@drawable/ic_volume_mute_white_24dp"
+            android:contentDescription="@string/nc_call_button_content_description_audio_output" />
+
+        <ImageButton
             android:id="@+id/cameraButton"
             android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_marginStart="10dp"
-            android:layout_marginEnd="10dp"
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:layout_marginHorizontal="@dimen/call_controls_margin_horizontal"
+            android:layout_weight="1"
             android:alpha="0.7"
-            app:backgroundImage="@color/call_buttons_background"
-            app:placeholderImage="@drawable/ic_videocam_white_24px"
-            app:roundAsCircle="true"
-            android:layout_weight="1"/>
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/call_buttons_background"
+            app:srcCompat="@drawable/ic_videocam_white_24px"
+            android:contentDescription="@string/nc_call_button_content_description_camera" />
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageButton
             android:id="@+id/microphoneButton"
             android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_marginStart="10dp"
-            android:layout_marginEnd="10dp"
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:layout_marginHorizontal="@dimen/call_controls_margin_horizontal"
+            android:layout_weight="1"
             android:alpha="0.7"
-            app:backgroundImage="@color/call_buttons_background"
-            app:placeholderImage="@drawable/ic_mic_off_white_24px"
-            app:roundAsCircle="true"
-            android:layout_weight="1"/>
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/call_buttons_background"
+            app:srcCompat="@drawable/ic_mic_off_white_24px"
+            android:contentDescription="@string/nc_call_button_content_description_microphone" />
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageButton
             android:id="@+id/hangupButton"
             android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_marginStart="10dp"
-            android:layout_marginEnd="20dp"
-            app:backgroundImage="@color/nc_darkRed"
-            app:placeholderImage="@drawable/ic_call_end_white_24px"
-            app:roundAsCircle="true"
-            android:layout_weight="1"/>
+            android:layout_height="wrap_content"
+            android:adjustViewBounds="true"
+            android:layout_marginHorizontal="@dimen/call_controls_margin_horizontal"
+            android:layout_weight="1"
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/nc_darkRed"
+            app:srcCompat="@drawable/ic_call_end_white_24px"
+            android:contentDescription="@string/nc_call_button_content_description_hangup" />
     </LinearLayout>
 
     <LinearLayout
         android:id="@+id/pipGroupCallOverlay"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:orientation="vertical"
         android:background="@color/black"
         android:gravity="center"
+        android:orientation="vertical"
         android:visibility="gone">
 
         <TextView
             android:id="@+id/pipCallConversationNameTextView"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginTop="-30dp"
-            android:layout_marginBottom="15dp"
             android:layout_marginStart="5dp"
+            android:layout_marginTop="-30dp"
             android:layout_marginEnd="5dp"
-            android:textAlignment="center"
-            android:maxLines="3"
+            android:layout_marginBottom="15dp"
             android:ellipsize="end"
+            android:maxLines="3"
+            android:textAlignment="center"
             android:textColor="@color/white"
             android:textSize="16sp"
             tools:text="our group call" />
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageView
             android:layout_width="80dp"
             android:layout_height="80dp"
-            app:backgroundImage="@drawable/ic_circular_group"
-            app:roundAsCircle="true" />
+            app:srcCompat="@drawable/ic_circular_group"
+            tools:ignore="ContentDescription" />
 
     </LinearLayout>
 

+ 2 - 2
app/src/main/res/layout/call_item.xml

@@ -31,12 +31,12 @@
     android:gravity="center"
     android:orientation="vertical">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@+id/avatarImageView"
         android:layout_width="80dp"
         android:layout_height="80dp"
         android:layout_centerInParent="true"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar"/>
 
     <org.webrtc.SurfaceViewRenderer
         android:id="@+id/surface_view"

+ 20 - 17
app/src/main/res/layout/call_notification_activity.xml

@@ -3,6 +3,8 @@
   ~
   ~ @author Mario Danic
   ~ @author Andy Scherzinger
+  ~ @author Tim Krüger
+  ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
   ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
   ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
   ~
@@ -36,36 +38,37 @@
         android:animateLayoutChanges="true"
         android:orientation="horizontal">
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageButton
             android:id="@+id/callAnswerVoiceOnlyView"
             android:layout_width="60dp"
             android:layout_height="60dp"
             android:layout_margin="24dp"
-            android:visibility="gone"
-            app:backgroundImage="@color/nc_darkGreen"
-            app:placeholderImage="@drawable/ic_call_white_24dp"
-            app:roundAsCircle="true"
-            tools:visibility="visible" />
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/nc_darkGreen"
+            android:src="@drawable/ic_call_white_24dp"
+            android:contentDescription="@string/nc_call_button_content_description_answer_voice_only" />
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageButton
             android:id="@+id/hangupButton"
             android:layout_width="60dp"
             android:layout_height="60dp"
             android:layout_margin="24dp"
-            app:backgroundImage="@color/nc_darkRed"
-            app:placeholderImage="@drawable/ic_call_end_white_24px"
-            app:roundAsCircle="true" />
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/nc_darkRed"
+            android:src="@drawable/ic_call_end_white_24px"
+            android:contentDescription="@string/nc_call_button_content_description_hangup" />
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageButton
             android:id="@+id/callAnswerCameraView"
             android:layout_width="60dp"
             android:layout_height="60dp"
             android:layout_margin="24dp"
+            android:background="@drawable/shape_oval"
+            android:backgroundTint="@color/nc_darkGreen"
+            android:src="@drawable/ic_videocam_white_24px"
             android:visibility="gone"
-            app:backgroundImage="@color/nc_darkGreen"
-            app:placeholderImage="@drawable/ic_videocam_white_24px"
-            app:roundAsCircle="true"
-            tools:visibility="visible" />
+            tools:visibility="visible"
+            android:contentDescription="@string/nc_call_button_content_description_answer_video_call" />
     </LinearLayout>
 
     <RelativeLayout
@@ -111,11 +114,11 @@
 
     </RelativeLayout>
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@+id/avatarImageView"
         android:layout_width="@dimen/avatar_size_big"
         android:layout_height="@dimen/avatar_size_big"
         android:layout_centerInParent="true"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar" />
 
 </RelativeLayout>

+ 3 - 3
app/src/main/res/layout/controller_conversation_info.xml

@@ -77,13 +77,13 @@
                         android:layout_marginTop="@dimen/margin_between_elements"
                         tools:text="Jane Doe" />
 
-                    <com.facebook.drawee.view.SimpleDraweeView
+                    <ImageView
                         android:id="@+id/avatar_image"
                         android:layout_width="@dimen/avatar_size_big"
                         android:layout_height="@dimen/avatar_size_big"
                         android:layout_centerHorizontal="true"
-                        apc:roundAsCircle="true"
-                        tools:background="@color/hwSecurityRed" />
+                        tools:background="@color/hwSecurityRed"
+                        android:contentDescription="@string/avatar" />
 
                 </RelativeLayout>
             </com.yarolegovich.mp.MaterialPreferenceCategory>

+ 2 - 5
app/src/main/res/layout/controller_profile.xml

@@ -19,7 +19,6 @@
 -->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:fresco="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -31,7 +30,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageView
             android:id="@+id/avatar_image"
             android:layout_width="@dimen/avatar_size_big"
             android:layout_height="@dimen/avatar_size_big"
@@ -39,9 +38,7 @@
             android:layout_marginTop="@dimen/standard_margin"
             android:src="@drawable/account_circle_96dp"
             android:transitionName="userAvatar.transitionTag"
-            app:roundAsCircle="true"
-            fresco:failureImage="@drawable/account_circle_96dp"
-            fresco:placeholderImage="@drawable/account_circle_96dp" />
+            android:contentDescription="@string/avatar" />
 
         <androidx.emoji.widget.EmojiTextView
             android:id="@+id/userinfo_fullName"

+ 2 - 5
app/src/main/res/layout/controller_settings.xml

@@ -25,7 +25,6 @@
 <com.yarolegovich.mp.MaterialPreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:apc="http://schemas.android.com/apk/res-auto"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:fresco="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     tools:background="@color/white"
     android:id="@+id/settings_screen"
@@ -119,16 +118,14 @@
 
             </com.google.android.material.card.MaterialCardView>
 
-            <com.facebook.drawee.view.SimpleDraweeView
+            <ImageView
                 android:id="@+id/avatar_image"
                 android:layout_width="@dimen/avatar_size_big"
                 android:layout_height="@dimen/avatar_size_big"
                 android:layout_centerHorizontal="true"
                 android:src="@drawable/account_circle_96dp"
                 android:transitionName="userAvatar.transitionTag"
-                apc:roundAsCircle="true"
-                fresco:failureImage="@drawable/account_circle_96dp"
-                fresco:placeholderImage="@drawable/account_circle_96dp" />
+                android:contentDescription="@string/avatar" />
 
             <com.yarolegovich.mp.MaterialStandardPreference
                 android:id="@+id/settings_remove_account"

+ 2 - 6
app/src/main/res/layout/current_account_item.xml

@@ -21,7 +21,6 @@
 -->
 <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:fresco="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="72dp"
@@ -45,7 +44,7 @@
             android:layout_alignParentStart="true"
             android:layout_centerVertical="true">
 
-            <com.facebook.drawee.view.SimpleDraweeView
+            <ImageView
                 android:id="@+id/user_icon"
                 android:layout_width="@dimen/small_item_height"
                 android:layout_height="@dimen/small_item_height"
@@ -55,10 +54,7 @@
                 android:layout_marginEnd="1dp"
                 android:layout_marginBottom="1dp"
                 android:contentDescription="@string/avatar"
-                android:src="@drawable/account_circle_48dp"
-                fresco:placeholderImage="@drawable/account_circle_48dp"
-                fresco:failureImage="@drawable/account_circle_48dp"
-                app:roundAsCircle="true"/>
+                android:src="@drawable/account_circle_48dp" />
 
             <ImageView
                 android:id="@+id/ticker"

+ 2 - 2
app/src/main/res/layout/item_custom_incoming_link_preview_message.xml

@@ -32,13 +32,13 @@
     android:layout_marginRight="16dp"
     android:layout_marginBottom="2dp">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@id/messageUserAvatar"
         android:layout_width="24dp"
         android:layout_height="24dp"
         android:layout_alignParentTop="true"
         android:layout_marginEnd="8dp"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar" />
 
     <com.google.android.flexbox.FlexboxLayout
         android:id="@id/bubble"

+ 2 - 2
app/src/main/res/layout/item_custom_incoming_location_message.xml

@@ -31,13 +31,13 @@
     android:layout_marginEnd="@dimen/standard_margin"
     android:layout_marginBottom="2dp">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@id/messageUserAvatar"
         android:layout_width="24dp"
         android:layout_height="24dp"
         android:layout_alignParentTop="true"
         android:layout_marginEnd="8dp"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar" />
 
     <com.google.android.flexbox.FlexboxLayout
         android:id="@id/bubble"

+ 2 - 2
app/src/main/res/layout/item_custom_incoming_poll_message.xml

@@ -28,13 +28,13 @@
     android:layout_marginRight="16dp"
     android:layout_marginBottom="2dp">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@id/messageUserAvatar"
         android:layout_width="24dp"
         android:layout_height="24dp"
         android:layout_alignParentTop="true"
         android:layout_marginEnd="8dp"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar" />
 
     <com.google.android.flexbox.FlexboxLayout
         android:id="@id/bubble"

+ 10 - 12
app/src/main/res/layout/item_custom_incoming_preview_message.xml

@@ -32,13 +32,13 @@
     android:layout_marginEnd="14dp"
     android:layout_marginBottom="2dp">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@id/messageUserAvatar"
         android:layout_width="24dp"
         android:layout_height="24dp"
         android:layout_alignParentTop="true"
         android:layout_marginEnd="6dp"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar" />
 
     <com.google.android.flexbox.FlexboxLayout
         android:layout_width="wrap_content"
@@ -64,14 +64,13 @@
             app:layout_wrapBefore="true"
             tools:visibility="gone">
 
-            <!-- SimpleDraweeView does not support wrap_content for layout_width or layout_height attributes! -->
-            <com.facebook.drawee.view.SimpleDraweeView
+            <ImageView
                 android:id="@id/image"
-                android:layout_width="100dp"
-                android:layout_height="100dp"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
                 android:scaleType="fitStart"
-                app:roundedCornerRadius="6dp"
-                tools:src="@drawable/ic_call_black_24dp" />
+                tools:src="@drawable/ic_call_black_24dp"
+                tools:ignore="ContentDescription" />
 
             <ProgressBar
                 android:id="@+id/progress_bar"
@@ -105,14 +104,13 @@
                     android:layout_height="wrap_content"
                     android:adjustViewBounds="true">
 
-                    <!-- SimpleDraweeView does not support wrap_content for layout_width or layout_height attributes! -->
-                    <com.facebook.drawee.view.SimpleDraweeView
+                    <ImageView
                         android:id="@+id/contact_photo"
                         android:layout_width="@dimen/small_item_height"
                         android:layout_height="@dimen/small_item_height"
                         android:scaleType="fitStart"
-                        app:roundAsCircle="true"
-                        tools:src="@drawable/ic_call_black_24dp" />
+                        tools:src="@drawable/ic_call_black_24dp"
+                        tools:ignore="ContentDescription" />
 
                     <ProgressBar
                         android:id="@+id/contact_progress_bar"

+ 2 - 2
app/src/main/res/layout/item_custom_incoming_text_message.xml

@@ -30,13 +30,13 @@
     android:layout_marginRight="16dp"
     android:layout_marginBottom="2dp">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@id/messageUserAvatar"
         android:layout_width="24dp"
         android:layout_height="24dp"
         android:layout_alignParentTop="true"
         android:layout_marginEnd="8dp"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar" />
 
     <com.google.android.flexbox.FlexboxLayout
         android:id="@id/bubble"

+ 2 - 2
app/src/main/res/layout/item_custom_incoming_voice_message.xml

@@ -32,13 +32,13 @@
     android:layout_marginRight="16dp"
     android:layout_marginBottom="2dp">
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageView
             android:id="@id/messageUserAvatar"
             android:layout_width="24dp"
             android:layout_height="24dp"
             android:layout_alignParentTop="true"
             android:layout_marginEnd="8dp"
-            app:roundAsCircle="true" />
+            android:contentDescription="@string/avatar" />
 
     <com.google.android.flexbox.FlexboxLayout
         android:id="@id/bubble"

+ 9 - 11
app/src/main/res/layout/item_custom_outcoming_preview_message.xml

@@ -55,14 +55,13 @@
             app:layout_flexGrow="1"
             app:layout_wrapBefore="true">
 
-            <!-- SimpleDraweeView does not support wrap_content for layout_width or layout_height attributes! -->
-            <com.facebook.drawee.view.SimpleDraweeView
+            <ImageView
                 android:id="@id/image"
-                android:layout_width="100dp"
-                android:layout_height="100dp"
-                android:scaleType="fitEnd"
-                app:roundedCornerRadius="6dp"
-                tools:src="@drawable/ic_call_black_24dp" />
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:scaleType="fitStart"
+                tools:src="@drawable/ic_call_black_24dp"
+                tools:ignore="ContentDescription" />
 
             <ProgressBar
                 android:id="@+id/progress_bar"
@@ -95,14 +94,13 @@
                     android:layout_height="wrap_content"
                     android:adjustViewBounds="true">
 
-                    <!-- SimpleDraweeView does not support wrap_content for layout_width or layout_height attributes! -->
-                    <com.facebook.drawee.view.SimpleDraweeView
+                    <ImageView
                         android:id="@+id/contact_photo"
                         android:layout_width="@dimen/small_item_height"
                         android:layout_height="@dimen/small_item_height"
                         android:scaleType="fitStart"
-                        app:roundAsCircle="true"
-                        tools:src="@drawable/ic_call_black_24dp" />
+                        tools:src="@drawable/ic_call_black_24dp"
+                        tools:ignore="ContentDescription" />
 
                     <ProgressBar
                         android:id="@+id/contact_progress_bar"

+ 2 - 3
app/src/main/res/layout/poll_result_voter_item.xml

@@ -19,20 +19,19 @@
   -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:paddingBottom="4dp"
     tools:background="@color/white">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@+id/poll_voter_avatar"
         android:layout_width="32dp"
         android:layout_height="32dp"
         android:layout_marginEnd="8dp"
         android:layout_gravity="center"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar"/>
 
     <TextView
         android:id="@+id/poll_voter_name"

+ 2 - 3
app/src/main/res/layout/reaction_item.xml

@@ -19,19 +19,18 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="@dimen/item_height"
     android:background="?android:attr/selectableItemBackground">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@+id/avatar"
         android:layout_width="@dimen/avatar_size"
         android:layout_height="@dimen/avatar_size"
         android:layout_gravity="center_vertical"
         android:layout_margin="@dimen/standard_margin"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar"/>
 
     <TextView
         android:id="@+id/name"

+ 21 - 22
app/src/main/res/layout/reference_inside_message.xml

@@ -19,7 +19,6 @@
 -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/referenceWrapper"
     android:layout_width="match_parent"
@@ -32,64 +31,64 @@
         android:layout_height="wrap_content"
         android:layout_marginEnd="8dp"
         android:background="@color/low_emphasis_text"
-        tools:layout_height="100dp"/>
+        tools:layout_height="100dp" />
 
     <androidx.emoji.widget.EmojiTextView
         android:id="@+id/referenceName"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:lineSpacingMultiplier="1.2"
-        android:textAlignment="viewStart"
-        android:textIsSelectable="false"
         android:layout_marginStart="10dp"
-        android:visibility="gone"
         android:ellipsize="end"
+        android:lineSpacingMultiplier="1.2"
         android:maxLines="2"
+        android:textAlignment="viewStart"
+        android:textIsSelectable="false"
         android:textStyle="bold"
+        android:visibility="gone"
         tools:text="Name of Website"
-        tools:visibility="visible"/>
+        tools:visibility="visible" />
 
     <androidx.emoji.widget.EmojiTextView
         android:id="@+id/referenceDescription"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_below="@id/referenceName"
+        android:layout_marginStart="10dp"
+        android:ellipsize="end"
         android:lineSpacingMultiplier="1.2"
+        android:maxLines="2"
         android:textAlignment="viewStart"
         android:textIsSelectable="false"
-        android:layout_marginStart="10dp"
         android:visibility="gone"
-        android:ellipsize="end"
-        android:maxLines="2"
         tools:text="Description of Website"
-        tools:visibility="visible"/>
+        tools:visibility="visible" />
 
     <androidx.emoji.widget.EmojiTextView
         android:id="@+id/referenceLink"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_below="@id/referenceDescription"
+        android:layout_marginStart="10dp"
+        android:ellipsize="end"
         android:lineSpacingMultiplier="1.2"
+        android:lines="1"
+        android:singleLine="true"
         android:textAlignment="viewStart"
-        android:textIsSelectable="false"
-        android:layout_marginStart="10dp"
         android:textColor="@color/medium_emphasis_text"
-        android:singleLine="true"
-        android:lines="1"
-        android:ellipsize="end"
+        android:textIsSelectable="false"
         android:visibility="gone"
         tools:text="http://nextcloud.com"
-        tools:visibility="visible"/>
+        tools:visibility="visible" />
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@+id/referenceThumbImage"
         android:layout_width="match_parent"
         android:layout_height="120dp"
-        android:scaleType="fitEnd"
         android:layout_below="@id/referenceLink"
-        android:layout_marginTop="5dp"
         android:layout_marginStart="10dp"
+        android:layout_marginTop="5dp"
+        android:scaleType="fitEnd"
         android:visibility="gone"
-        app:roundedCornerRadius="6dp"
-        tools:visibility="visible"/>
+        tools:visibility="visible"
+        tools:ignore="ContentDescription" />
 </RelativeLayout>

+ 2 - 3
app/src/main/res/layout/rv_item_browser_file.xml

@@ -93,13 +93,12 @@
         android:textSize="@dimen/two_line_primary_text_size"
         tools:text="filename.md" />
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@+id/file_icon"
         android:layout_width="40dp"
         android:layout_height="40dp"
         android:layout_centerVertical="true"
         android:layout_marginEnd="@dimen/standard_margin"
-        app:actualImageScaleType="fitCenter"
-        app:placeholderImageScaleType="fitCenter" />
+        tools:ignore="ContentDescription" />
 
 </RelativeLayout>

+ 4 - 4
app/src/main/res/layout/rv_item_contact.xml

@@ -48,19 +48,19 @@
         android:layout_height="wrap_content"
         android:layout_centerVertical="true"
         android:layout_toStartOf="@id/checkedImageView"
-        android:layout_toEndOf="@id/avatar_drawee_view"
+        android:layout_toEndOf="@id/avatar_view"
         android:ellipsize="end"
         android:lines="1"
         android:textAlignment="viewStart"
         android:textAppearance="@style/ListItem"
         tools:text="Jane Doe" />
 
-    <com.facebook.drawee.view.SimpleDraweeView
-        android:id="@+id/avatar_drawee_view"
+    <ImageView
+        android:id="@+id/avatar_view"
         android:layout_width="@dimen/avatar_size"
         android:layout_height="@dimen/avatar_size"
         android:layout_centerVertical="true"
         android:layout_marginEnd="@dimen/standard_margin"
-        app:roundAsCircle="true" />
+        android:contentDescription="@string/avatar" />
 
 </RelativeLayout>

+ 2 - 2
app/src/main/res/layout/rv_item_contact_shimmer.xml

@@ -29,7 +29,7 @@
     android:orientation="vertical">
 
     <com.elyeproj.loaderviewlibrary.LoaderTextView
-        android:id="@+id/simple_drawee_view"
+        android:id="@+id/loader_text_view"
         android:layout_width="@dimen/avatar_size"
         android:layout_height="@dimen/avatar_size"
         android:layout_centerVertical="true"
@@ -42,7 +42,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_centerVertical="true"
-        android:layout_toEndOf="@id/simple_drawee_view"
+        android:layout_toEndOf="@id/loader_text_view"
         app:custom_color="@color/nc_shimmer_default_color" />
 
 </RelativeLayout>

+ 7 - 8
app/src/main/res/layout/rv_item_conversation_info_participant.xml

@@ -26,15 +26,14 @@
     android:layout_marginBottom="@dimen/standard_half_margin"
     android:layout_marginTop="@dimen/standard_margin">
 
-    <com.facebook.drawee.view.SimpleDraweeView
-        android:id="@+id/avatar_drawee_view"
+    <ImageView
+        android:id="@+id/avatar_view"
         android:layout_width="@dimen/small_item_height"
         android:layout_height="@dimen/small_item_height"
         android:layout_marginStart="@dimen/standard_margin"
         android:contentDescription="@null"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:roundAsCircle="true" />
+        app:layout_constraintTop_toTopOf="parent" />
 
     <com.vanniktech.emoji.EmojiEditText
         android:id="@+id/participant_status_emoji"
@@ -54,8 +53,8 @@
         android:layout_height="18dp"
         android:layout_gravity="bottom|end"
         android:contentDescription="@string/nc_account_chooser_active_user"
-        app:layout_constraintBottom_toBottomOf="@+id/avatar_drawee_view"
-        app:layout_constraintEnd_toEndOf="@+id/avatar_drawee_view"
+        app:layout_constraintBottom_toBottomOf="@+id/avatar_view"
+        app:layout_constraintEnd_toEndOf="@+id/avatar_view"
         tools:src="@drawable/emoji_one_category_smileysandpeople"/>
 
     <androidx.emoji.widget.EmojiTextView
@@ -68,8 +67,8 @@
         android:textAlignment="viewStart"
         android:textAppearance="?android:attr/textAppearanceListItem"
         android:textColor="@color/conversation_item_header"
-        app:layout_constraintStart_toEndOf="@id/avatar_drawee_view"
-        app:layout_constraintTop_toTopOf="@+id/avatar_drawee_view"
+        app:layout_constraintStart_toEndOf="@id/avatar_view"
+        app:layout_constraintTop_toTopOf="@+id/avatar_view"
         tools:text="Jane Doe" />
 
     <androidx.emoji.widget.EmojiTextView

+ 4 - 3
app/src/main/res/layout/rv_item_conversation_with_last_message.xml

@@ -3,6 +3,8 @@
   ~
   ~ @author Mario Danic
   ~ @author Andy Scherzinger
+  ~ @author Tim Krüger
+  ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
   ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
   ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
   ~
@@ -38,12 +40,11 @@
         android:layout_centerVertical="true"
         android:layout_marginEnd="@dimen/double_margin_between_elements">
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageView
             android:id="@id/dialogAvatar"
             android:layout_width="@dimen/small_item_height"
             android:layout_height="@dimen/small_item_height"
-            android:contentDescription="@null"
-            app:roundAsCircle="true" />
+            android:contentDescription="@null" />
 
         <ImageView
             android:id="@+id/favoriteConversationImageView"

+ 1 - 2
app/src/main/res/layout/rv_item_load_more.xml

@@ -41,8 +41,7 @@
         android:layout_height="@dimen/small_item_height"
         android:importantForAccessibility="no"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:roundAsCircle="true" />
+        app:layout_constraintTop_toTopOf="parent" />
 
     <TextView
         android:layout_width="0dp"

+ 4 - 3
app/src/main/res/layout/rv_item_search_message.xml

@@ -3,6 +3,8 @@
   ~
   ~ @author Mario Danic
   ~ @author Andy Scherzinger
+  ~ @author Tim Krüger
+  ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
   ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
   ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
   ~
@@ -35,14 +37,13 @@
     android:layout_margin="@dimen/double_margin_between_elements"
     tools:background="@color/white">
 
-    <com.facebook.drawee.view.SimpleDraweeView
+    <ImageView
         android:id="@+id/thumbnail"
         android:layout_width="@dimen/small_item_height"
         android:layout_height="@dimen/small_item_height"
         android:importantForAccessibility="no"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:roundAsCircle="true" />
+        app:layout_constraintTop_toTopOf="parent" />
 
     <androidx.emoji.widget.EmojiTextView
         android:id="@+id/conversation_title"

+ 3 - 5
app/src/main/res/layout/shared_item_grid.xml

@@ -22,7 +22,7 @@
 
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:fresco="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
@@ -40,14 +40,12 @@
         app:layout_flexGrow="1"
         app:layout_wrapBefore="true">
 
-        <com.facebook.drawee.view.SimpleDraweeView
+        <ImageView
             android:id="@+id/image"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:padding="4dp"
-            app:placeholderImageScaleType="fitCenter"
-            fresco:actualImageScaleType="centerCrop"
-            fresco:roundedCornerRadius="4dp" />
+            tools:ignore="ContentDescription" />
 
         <ProgressBar
             android:id="@+id/progress_bar"

+ 3 - 6
app/src/main/res/layout/shared_item_list.xml

@@ -39,17 +39,14 @@
         app:layout_flexGrow="1"
         app:layout_wrapBefore="true">
 
-        <com.facebook.drawee.view.SimpleDraweeView
-            xmlns:fresco="http://schemas.android.com/apk/res-auto"
+        <ImageView
             android:id="@+id/file_image"
             android:layout_width="@dimen/mediatab_file_icon_size"
             android:layout_height="@dimen/mediatab_file_icon_size"
             android:padding="4dp"
             app:layout_constraintTop_toTopOf="parent"
-            app:placeholderImageScaleType="fitCenter"
-            fresco:actualImageScaleType="centerCrop"
-            fresco:roundedCornerRadius="4dp"
-            tools:src="@drawable/ic_call_black_24dp"/>
+            tools:src="@drawable/ic_call_black_24dp"
+            tools:ignore="ContentDescription" />
 
         <ProgressBar
             android:id="@+id/progress_bar"

+ 4 - 0
app/src/main/res/values/dimens.xml

@@ -2,6 +2,8 @@
   ~ Nextcloud Talk application
   ~
   ~ @author Mario Danic
+  ~ @author Tim Krüger
+  ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
   ~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
   ~
   ~ This program is free software: you can redistribute it and/or modify
@@ -67,6 +69,8 @@
     <dimen name="call_self_video_short_side_length">80dp</dimen>
     <dimen name="call_grid_item_min_height">180dp</dimen>
     <dimen name="call_controls_height">110dp</dimen>
+    <dimen name="call_controls_padding_horizontal">10dp</dimen>
+    <dimen name="call_controls_margin_horizontal">10dp</dimen>
     <dimen name="call_participant_progress_bar_size">48dp</dimen>
     <dimen name="call_self_participant_progress_bar_size">48dp</dimen>
     <dimen name="zero">0dp</dimen>

+ 10 - 0
app/src/main/res/values/strings.xml

@@ -4,6 +4,8 @@
   ~ @author Mario Danic
   ~ @author Andy Scherzinger
   ~ @author Marcel Hibbe
+  ~ @author Tim Krüger
+  ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
   ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
   ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
   ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
@@ -211,6 +213,14 @@
     <string name="nc_call_state_with_phone">%1$s with phone</string>
     <string name="nc_call_state_with_video">%1$s with video</string>
     <string name="nc_missed_call">You missed a call from %s</string>
+    <string name="nc_call_button_content_description_pip">Open picture in picture mode</string>
+    <string name="nc_call_button_content_description_audio_output">Change audio output</string>
+    <string name="nc_call_button_content_description_camera">Toggle camera</string>
+    <string name="nc_call_button_content_description_microphone">Toggle microphone</string>
+    <string name="nc_call_button_content_description_hangup">Hangup</string>
+    <string name="nc_call_button_content_description_answer_voice_only">Answer as voice call only</string>
+    <string name="nc_call_button_content_description_answer_video_call">Answer as video call</string>
+    <string name="nc_call_button_content_description_switch_to_self_vide">Switch to self video</string>
 
     <!-- Picture in Picture -->
     <string name="nc_pip_microphone_mute">Mute microphone</string>