Преглед изворни кода

Merge pull request #2449 from nextcloud/bugfix/2376/fresco-blocks-increase-of-minsdkversion

Set minSdkVersion to 23 (Android 6) II
Tim Krüger пре 2 година
родитељ
комит
13f60d1b2d
92 измењених фајлова са 1454 додато и 1870 уклоњено
  1. 2 9
      app/build.gradle
  2. 1 4
      app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt
  3. 17 33
      app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
  4. 3 38
      app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt
  5. 1 7
      app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
  6. 3 12
      app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java
  7. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt
  8. 11 34
      app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt
  9. 2 20
      app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java
  10. 20 33
      app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java
  11. 0 367
      app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java
  12. 349 0
      app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt
  13. 10 24
      app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java
  14. 4 13
      app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
  15. 13 46
      app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java
  16. 4 25
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
  17. 4 20
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  18. 4 25
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
  19. 3 3
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
  20. 4 26
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt
  21. 2 13
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  22. 2 9
      app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
  23. 2 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
  24. 16 18
      app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt
  25. 10 21
      app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
  26. 1 1
      app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java
  27. 52 57
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  28. 8 48
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  29. 52 79
      app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt
  30. 3 7
      app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt
  31. 5 15
      app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt
  32. 27 38
      app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt
  33. 283 0
      app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt
  34. 4 9
      app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
  35. 2 8
      app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  36. 7 34
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt
  37. 10 49
      app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt
  38. 20 13
      app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt
  39. 7 16
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt
  40. 3 13
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt
  41. 2 2
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt
  42. 7 6
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt
  43. 13 44
      app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt
  44. 2 15
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java
  45. 2 19
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt
  46. 55 220
      app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
  47. 3 2
      app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt
  48. 2 2
      app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
  49. 32 27
      app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
  50. 0 41
      app/src/main/java/com/nextcloud/talk/utils/OkHttpNetworkFetcherWithCache.java
  51. 0 5
      app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java
  52. 4 9
      app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt
  53. 6 66
      app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt
  54. 2 1
      app/src/main/java/com/nextcloud/talk/utils/text/Spans.java
  55. 10 15
      app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java
  56. 1 1
      app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java
  57. 0 0
      app/src/main/java/third/parties/daveKoeller/lgpl-2.1.txt
  58. 138 0
      app/src/main/java/third/parties/fresco/BetterImageSpan.kt
  59. 5 0
      app/src/main/res/drawable/shape_oval.xml
  60. 2 6
      app/src/main/res/layout/account_item.xml
  61. 2 1
      app/src/main/res/layout/activity_main.xml
  62. 62 55
      app/src/main/res/layout/call_activity.xml
  63. 2 2
      app/src/main/res/layout/call_item.xml
  64. 20 17
      app/src/main/res/layout/call_notification_activity.xml
  65. 3 3
      app/src/main/res/layout/controller_conversation_info.xml
  66. 2 5
      app/src/main/res/layout/controller_profile.xml
  67. 2 5
      app/src/main/res/layout/controller_settings.xml
  68. 2 6
      app/src/main/res/layout/current_account_item.xml
  69. 2 2
      app/src/main/res/layout/item_custom_incoming_link_preview_message.xml
  70. 2 2
      app/src/main/res/layout/item_custom_incoming_location_message.xml
  71. 2 2
      app/src/main/res/layout/item_custom_incoming_poll_message.xml
  72. 10 12
      app/src/main/res/layout/item_custom_incoming_preview_message.xml
  73. 2 2
      app/src/main/res/layout/item_custom_incoming_text_message.xml
  74. 2 2
      app/src/main/res/layout/item_custom_incoming_voice_message.xml
  75. 9 11
      app/src/main/res/layout/item_custom_outcoming_preview_message.xml
  76. 4 5
      app/src/main/res/layout/poll_result_voter_item.xml
  77. 2 3
      app/src/main/res/layout/reaction_item.xml
  78. 21 22
      app/src/main/res/layout/reference_inside_message.xml
  79. 2 3
      app/src/main/res/layout/rv_item_browser_file.xml
  80. 4 4
      app/src/main/res/layout/rv_item_contact.xml
  81. 2 2
      app/src/main/res/layout/rv_item_contact_shimmer.xml
  82. 7 8
      app/src/main/res/layout/rv_item_conversation_info_participant.xml
  83. 4 3
      app/src/main/res/layout/rv_item_conversation_with_last_message.xml
  84. 1 2
      app/src/main/res/layout/rv_item_load_more.xml
  85. 4 3
      app/src/main/res/layout/rv_item_search_message.xml
  86. 3 5
      app/src/main/res/layout/shared_item_grid.xml
  87. 3 6
      app/src/main/res/layout/shared_item_list.xml
  88. 0 2
      app/src/main/res/values-night/colors.xml
  89. 0 3
      app/src/main/res/values/colors.xml
  90. 4 0
      app/src/main/res/values/dimens.xml
  91. 10 0
      app/src/main/res/values/strings.xml
  92. 1 1
      scripts/analysis/lint-results.txt

+ 2 - 9
app/build.gradle

@@ -42,7 +42,7 @@ android {
     namespace 'com.nextcloud.talk'
 
     defaultConfig {
-        minSdkVersion 21
+        minSdkVersion 23
         targetSdkVersion 31
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 
@@ -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}"

+ 1 - 4
app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt

@@ -22,7 +22,6 @@ package com.nextcloud.talk.activities
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.os.Build
 import android.os.Bundle
 import android.util.Log
 import android.view.WindowManager
@@ -76,9 +75,7 @@ open class BaseActivity : AppCompatActivity() {
         }
 
         if (appPreferences.isScreenLocked) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                SecurityUtils.createKey(appPreferences.screenLockTimeout)
-            }
+            SecurityUtils.createKey(appPreferences.screenLockTimeout)
         }
     }
 

+ 17 - 33
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");
@@ -737,10 +732,8 @@ public class CallActivity extends CallBaseActivity {
         } else {
             if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CALL)) {
                 onPermissionsGranted();
-            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                requestPermissions(PERMISSIONS_CALL, 100);
             } else {
-                onRequestPermissionsResult(100, PERMISSIONS_CALL, new int[]{1, 1});
+                requestPermissions(PERMISSIONS_CALL, 100);
             }
         }
 
@@ -795,7 +788,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 +799,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 +910,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 +954,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 +967,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);
             }
@@ -985,11 +978,7 @@ public class CallActivity extends CallBaseActivity {
                 R.string.nc_microphone_permission_permanently_denied,
                 R.string.nc_permissions_settings, (AppCompatActivity) this);
         } else {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                requestPermissions(PERMISSIONS_MICROPHONE, 100);
-            } else {
-                onRequestPermissionsResult(100, PERMISSIONS_MICROPHONE, new int[]{1});
-            }
+            requestPermissions(PERMISSIONS_MICROPHONE, 100);
         }
     }
 
@@ -997,7 +986,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 +995,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);
             }
 
@@ -1022,12 +1011,7 @@ public class CallActivity extends CallBaseActivity {
                 R.string.nc_camera_permission_permanently_denied,
                 R.string.nc_permissions_settings, (AppCompatActivity) this);
         } else {
-
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                requestPermissions(PERMISSIONS_CAMERA, 100);
-            } else {
-                onRequestPermissionsResult(100, PERMISSIONS_CAMERA, new int[]{1});
-            }
+            requestPermissions(PERMISSIONS_CAMERA, 100);
         }
 
     }
@@ -2675,7 +2659,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) {

+ 1 - 7
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt

@@ -24,12 +24,10 @@ package com.nextcloud.talk.activities
 import android.app.KeyguardManager
 import android.content.Context
 import android.content.Intent
-import android.os.Build
 import android.os.Bundle
 import android.provider.ContactsContract
 import android.text.TextUtils
 import android.util.Log
-import androidx.annotation.RequiresApi
 import autodagger.AutoInjector
 import com.bluelinelabs.conductor.Conductor
 import com.bluelinelabs.conductor.Router
@@ -148,10 +146,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
         super.onStart()
         Log.d(TAG, "onStart: Activity: " + System.identityHashCode(this).toString())
         logRouterBackStack(router!!)
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            lockScreenIfConditionsApply()
-        }
+        lockScreenIfConditionsApply()
     }
 
     override fun onResume() {
@@ -323,7 +318,6 @@ class MainActivity : BaseActivity(), ActionBarProvider {
             })
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     fun lockScreenIfConditionsApply() {
         val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
         if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {

+ 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

+ 0 - 367
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java

@@ -1,367 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * @author Andy Scherzinger
- * @author Marcel Hibbe
- * 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>
- *
- * 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.adapters.items;
-
-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 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.RvItemConversationWithLastMessageBinding;
-import com.nextcloud.talk.models.json.chat.ChatMessage;
-import com.nextcloud.talk.models.json.conversations.Conversation;
-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;
-
-import java.util.List;
-import java.util.regex.Pattern;
-
-import androidx.core.content.ContextCompat;
-import androidx.core.content.res.ResourcesCompat;
-import eu.davidea.flexibleadapter.FlexibleAdapter;
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
-import eu.davidea.flexibleadapter.items.IFilterable;
-import eu.davidea.flexibleadapter.items.IFlexible;
-import eu.davidea.flexibleadapter.items.ISectionable;
-import eu.davidea.viewholders.FlexibleViewHolder;
-
-public class ConversationItem extends AbstractFlexibleItem<ConversationItem.ConversationItemViewHolder> implements
-    ISectionable<ConversationItem.ConversationItemViewHolder, GenericTextHeaderItem>, IFilterable<String> {
-
-    public static final int VIEW_TYPE = R.layout.rv_item_conversation_with_last_message;
-
-    private static final float STATUS_SIZE_IN_DP = 9f;
-
-    private final Conversation conversation;
-    private final User user;
-    private final Context context;
-    private GenericTextHeaderItem header;
-    private final ViewThemeUtils viewThemeUtils;
-
-
-    public ConversationItem(Conversation conversation, User user, Context activityContext, final ViewThemeUtils viewThemeUtils) {
-        this.conversation = conversation;
-        this.user = user;
-        this.context = activityContext;
-        this.viewThemeUtils = viewThemeUtils;
-    }
-
-    public ConversationItem(Conversation conversation, User user,
-                            Context activityContext, GenericTextHeaderItem genericTextHeaderItem,
-                            final ViewThemeUtils viewThemeUtils) {
-        this(conversation, user, activityContext, viewThemeUtils);
-        this.header = genericTextHeaderItem;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o instanceof ConversationItem) {
-            ConversationItem inItem = (ConversationItem) o;
-            return conversation.equals(inItem.getModel());
-        }
-        return false;
-    }
-
-    public Conversation getModel() {
-        return conversation;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = conversation.hashCode();
-        result = 31 * result;
-        return result;
-    }
-
-    @Override
-    public int getLayoutRes() {
-        return R.layout.rv_item_conversation_with_last_message;
-    }
-
-    @Override
-    public int getItemViewType() {
-        return VIEW_TYPE;
-    }
-
-    @Override
-    public ConversationItemViewHolder createViewHolder(View view, FlexibleAdapter<IFlexible> adapter) {
-        return new ConversationItemViewHolder(view, adapter);
-    }
-
-    @SuppressLint("SetTextI18n")
-    @Override
-    public void bindViewHolder(FlexibleAdapter<IFlexible> adapter,
-                               ConversationItemViewHolder holder,
-                               int position,
-                               List<Object> payloads) {
-        Context appContext =
-            NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext();
-        holder.binding.dialogAvatar.setController(null);
-
-        holder.binding.dialogName.setTextColor(ResourcesCompat.getColor(context.getResources(),
-                                                                        R.color.conversation_item_header,
-                                                                        null));
-
-        if (adapter.hasFilter()) {
-            viewThemeUtils.platform.highlightText(holder.binding.dialogName,
-                                         conversation.getDisplayName(),
-                                         String.valueOf(adapter.getFilter(String.class)));
-        } else {
-            holder.binding.dialogName.setText(conversation.getDisplayName());
-        }
-
-        if (conversation.getUnreadMessages() > 0) {
-            holder.binding.dialogName.setTypeface(holder.binding.dialogName.getTypeface(), Typeface.BOLD);
-            holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.getTypeface(), Typeface.BOLD);
-            holder.binding.dialogUnreadBubble.setVisibility(View.VISIBLE);
-            if (conversation.getUnreadMessages() < 1000) {
-                holder.binding.dialogUnreadBubble.setText(Long.toString(conversation.getUnreadMessages()));
-            } else {
-                holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages);
-            }
-
-            ColorStateList lightBubbleFillColor = ColorStateList.valueOf(
-                ContextCompat.getColor(context,
-                                       R.color.conversation_unread_bubble));
-            int lightBubbleTextColor = ContextCompat.getColor(
-                context,
-                R.color.conversation_unread_bubble_text);
-
-            if (conversation.getType() == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
-                viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble);
-            } else if (conversation.getUnreadMention()) {
-                if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "direct-mention-flag")) {
-                    if (conversation.getUnreadMentionDirect()) {
-                        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.setChipBackgroundColor(lightBubbleFillColor);
-                holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor);
-            }
-        } else {
-            holder.binding.dialogName.setTypeface(null, Typeface.NORMAL);
-            holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL);
-            holder.binding.dialogLastMessage.setTypeface(null, Typeface.NORMAL);
-            holder.binding.dialogUnreadBubble.setVisibility(View.GONE);
-        }
-
-        if (conversation.getFavorite()) {
-            holder.binding.favoriteConversationImageView.setVisibility(View.VISIBLE);
-        } else {
-            holder.binding.favoriteConversationImageView.setVisibility(View.GONE);
-        }
-
-        if (conversation.getStatus() != null && Conversation.ConversationType.ROOM_SYSTEM != conversation.getType()) {
-            float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext);
-
-            holder.binding.userStatusImage.setVisibility(View.VISIBLE);
-            holder.binding.userStatusImage.setImageDrawable(new StatusDrawable(
-                conversation.getStatus(),
-                conversation.getStatusIcon(),
-                size,
-                context.getResources().getColor(R.color.bg_default),
-                appContext));
-        } else {
-            holder.binding.userStatusImage.setVisibility(View.GONE);
-        }
-
-        if (conversation.getLastMessage() != null) {
-            holder.binding.dialogDate.setVisibility(View.VISIBLE);
-            holder.binding.dialogDate.setText(
-                DateUtils.getRelativeTimeSpanString(conversation.getLastActivity() * 1000L,
-                                                    System.currentTimeMillis(),
-                                                    0,
-                                                    DateUtils.FORMAT_ABBREV_RELATIVE));
-
-            if (!TextUtils.isEmpty(conversation.getLastMessage().getSystemMessage()) ||
-                Conversation.ConversationType.ROOM_SYSTEM == conversation.getType()) {
-                holder.binding.dialogLastMessage.setText(conversation.getLastMessage().getText());
-            } else {
-                String authorDisplayName = "";
-                conversation.getLastMessage().setActiveUser(user);
-                String text;
-                if (conversation.getLastMessage().getCalculateMessageType() == ChatMessage.MessageType.REGULAR_TEXT_MESSAGE) {
-                    if (conversation.getLastMessage().getActorId().equals(user.getUserId())) {
-                        text = String.format(appContext.getString(R.string.nc_formatted_message_you),
-                                             conversation.getLastMessage().getLastMessageDisplayText());
-                    } else {
-                        authorDisplayName = !TextUtils.isEmpty(conversation.getLastMessage().getActorDisplayName()) ?
-                            conversation.getLastMessage().getActorDisplayName() :
-                            "guests".equals(conversation.getLastMessage().getActorType()) ?
-                                appContext.getString(R.string.nc_guest) : "";
-                        text = String.format(appContext.getString(R.string.nc_formatted_message),
-                                             authorDisplayName,
-                                             conversation.getLastMessage().getLastMessageDisplayText());
-                    }
-                } else {
-                    text = conversation.getLastMessage().getLastMessageDisplayText();
-                }
-
-                holder.binding.dialogLastMessage.setText(text);
-            }
-        } else {
-            holder.binding.dialogDate.setVisibility(View.GONE);
-            holder.binding.dialogLastMessage.setText(R.string.nc_no_messages_yet);
-        }
-
-        holder.binding.dialogAvatar.setVisibility(View.VISIBLE);
-
-        boolean shouldLoadAvatar = true;
-        String objectType;
-        if (!TextUtils.isEmpty(objectType = conversation.getObjectType())) {
-            switch (objectType) {
-                case "share:password":
-                    shouldLoadAvatar = false;
-                    holder.binding.dialogAvatar.setImageDrawable(
-                        ContextCompat.getDrawable(context,
-                                                  R.drawable.ic_circular_lock));
-                    break;
-                case "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)));
-                    } else {
-                        holder.binding.dialogAvatar.setImageDrawable(
-                            ContextCompat.getDrawable(context, R.drawable.ic_circular_document));
-                    }
-                    break;
-                default:
-                    break;
-            }
-        }
-
-        if (conversation.getType() == Conversation.ConversationType.ROOM_SYSTEM) {
-            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);
-
-                holder.binding.dialogAvatar.getHierarchy().setPlaceholderImage(
-                    DisplayUtils.getRoundedDrawable(layerDrawable));
-            } else {
-                holder.binding.dialogAvatar.getHierarchy().setPlaceholderImage(R.mipmap.ic_launcher);
-            }
-            shouldLoadAvatar = false;
-        }
-
-        if (shouldLoadAvatar) {
-            switch (conversation.getType()) {
-                case ROOM_TYPE_ONE_TO_ONE_CALL:
-                    if (!TextUtils.isEmpty(conversation.getName())) {
-                        DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                            .setOldController(holder.binding.dialogAvatar.getController())
-                            .setAutoPlayAnimations(true)
-                            .setImageRequest(DisplayUtils.getImageRequestForUrl(
-                                ApiUtils.getUrlForAvatar(user.getBaseUrl(),
-                                                         conversation.getName(),
-                                                         true),
-                                user))
-                            .build();
-                        holder.binding.dialogAvatar.setController(draweeController);
-                    } else {
-                        holder.binding.dialogAvatar.setVisibility(View.GONE);
-                    }
-                    break;
-                case 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)));
-                    } else {
-                        holder.binding.dialogAvatar.setImageDrawable(
-                            ContextCompat.getDrawable(context, R.drawable.ic_circular_group));
-                    }
-                    break;
-                case 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)));
-                    } else {
-                        holder.binding.dialogAvatar.setImageDrawable(
-                            ContextCompat.getDrawable(context, R.drawable.ic_circular_link));
-                    }
-                    break;
-                default:
-                    holder.binding.dialogAvatar.setVisibility(View.GONE);
-            }
-        }
-    }
-
-    @Override
-    public boolean filter(String constraint) {
-        return conversation.getDisplayName() != null &&
-            Pattern
-                .compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
-                .matcher(conversation.getDisplayName().trim())
-                .find();
-    }
-
-    @Override
-    public GenericTextHeaderItem getHeader() {
-        return header;
-    }
-
-    @Override
-    public void setHeader(GenericTextHeaderItem header) {
-        this.header = header;
-    }
-
-    static class ConversationItemViewHolder extends FlexibleViewHolder {
-
-        RvItemConversationWithLastMessageBinding binding;
-
-        ConversationItemViewHolder(View view, FlexibleAdapter adapter) {
-            super(view, adapter);
-            binding = RvItemConversationWithLastMessageBinding.bind(view);
-        }
-    }
-}

+ 349 - 0
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt

@@ -0,0 +1,349 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @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>
+ *
+ * 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.adapters.items
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Typeface
+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.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.DisplayUtils
+import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.flexibleadapter.items.IFilterable
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.davidea.flexibleadapter.items.ISectionable
+import eu.davidea.viewholders.FlexibleViewHolder
+import java.util.regex.Pattern
+
+class ConversationItem(
+    val model: Conversation,
+    private val user: User,
+    private val context: Context,
+    private val viewThemeUtils: ViewThemeUtils
+) : AbstractFlexibleItem<ConversationItemViewHolder>(),
+    ISectionable<ConversationItemViewHolder, GenericTextHeaderItem?>,
+    IFilterable<String?> {
+    private var header: GenericTextHeaderItem? = null
+
+    constructor(
+        conversation: Conversation,
+        user: User,
+        activityContext: Context,
+        genericTextHeaderItem: GenericTextHeaderItem?,
+        viewThemeUtils: ViewThemeUtils
+    ) : this(conversation, user, activityContext, viewThemeUtils) {
+        header = genericTextHeaderItem
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other is ConversationItem) {
+            return model == other.model
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        var result = model.hashCode()
+        result *= 31
+        return result
+    }
+
+    override fun getLayoutRes(): Int {
+        return R.layout.rv_item_conversation_with_last_message
+    }
+
+    override fun getItemViewType(): Int {
+        return VIEW_TYPE
+    }
+
+    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ConversationItemViewHolder {
+        return ConversationItemViewHolder(view, adapter)
+    }
+
+    @SuppressLint("SetTextI18n")
+    override fun bindViewHolder(
+        adapter: FlexibleAdapter<IFlexible<*>?>,
+        holder: ConversationItemViewHolder,
+        position: Int,
+        payloads: List<Any>
+    ) {
+        val appContext = sharedApplication!!.applicationContext
+        holder.binding.dialogName.setTextColor(
+            ResourcesCompat.getColor(
+                context.resources,
+                R.color.conversation_item_header,
+                null
+            )
+        )
+        if (adapter.hasFilter()) {
+            viewThemeUtils.platform.highlightText(
+                holder.binding.dialogName,
+                model.displayName!!, adapter.getFilter(String::class.java).toString()
+            )
+        } else {
+            holder.binding.dialogName.text = model.displayName
+        }
+        if (model.unreadMessages > 0) {
+            showUnreadMessages(holder)
+        } else {
+            holder.binding.dialogName.setTypeface(null, Typeface.NORMAL)
+            holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL)
+            holder.binding.dialogLastMessage.setTypeface(null, Typeface.NORMAL)
+            holder.binding.dialogUnreadBubble.visibility = View.GONE
+        }
+        if (model.favorite) {
+            holder.binding.favoriteConversationImageView.visibility = View.VISIBLE
+        } else {
+            holder.binding.favoriteConversationImageView.visibility = View.GONE
+        }
+        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.statusIcon,
+                    size,
+                    context.resources.getColor(R.color.bg_default),
+                    appContext
+                )
+            )
+        } else {
+            holder.binding.userStatusImage.visibility = View.GONE
+        }
+        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 {
+                    holder.binding.dialogAvatar.visibility = View.GONE
+                }
+                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
+            }
+        }
+    }
+
+    private fun shouldLoadAvatar(
+        holder: ConversationItemViewHolder
+    ): Boolean {
+        var objectType: String?
+        var returnValue = true
+        if (!TextUtils.isEmpty(model.objectType.also { objectType = it })) {
+            when (objectType) {
+                "share:password" -> {
+                    holder.binding.dialogAvatar.setImageDrawable(
+                        ContextCompat.getDrawable(
+                            context,
+                            R.drawable.ic_circular_lock
+                        )
+                    )
+                    returnValue = false
+                }
+                "file" -> {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                        holder.binding.dialogAvatar.loadAvatar(
+                            viewThemeUtils.talk.themePlaceholderAvatar(
+                                holder.binding.dialogAvatar,
+                                R.drawable.ic_avatar_document
+                            )
+                        )
+                    } else {
+                        holder.binding.dialogAvatar.loadAvatar(
+                            R.drawable.ic_circular_document
+                        )
+                    }
+                    returnValue = false
+                }
+            }
+        }
+        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 {
+                model.lastMessage!!.activeUser = user
+
+                val text = if (model.lastMessage!!.getCalculateMessageType() === ChatMessage.MessageType
+                    .REGULAR_TEXT_MESSAGE
+                ) {
+                    calculateRegularLastMessageText(appContext)
+                } else {
+                    model.lastMessage!!.lastMessageDisplayText
+                }
+                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 {
+                    ""
+                }
+            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 {
+                    viewThemeUtils.material.colorChipOutlined(
+                        holder.binding.dialogUnreadBubble,
+                        UNREAD_BUBBLE_STROKE_WIDTH
+                    )
+                }
+            } else {
+                viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
+            }
+        } else {
+            holder.binding.dialogUnreadBubble.chipBackgroundColor = lightBubbleFillColor
+            holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor)
+        }
+    }
+
+    override fun filter(constraint: String?): Boolean {
+        return model.displayName != null &&
+            Pattern
+                .compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
+                .matcher(model.displayName!!.trim { it <= ' ' })
+                .find()
+    }
+
+    override fun getHeader(): GenericTextHeaderItem? {
+        return header
+    }
+
+    override fun setHeader(header: GenericTextHeaderItem?) {
+        this.header = header
+    }
+
+    class ConversationItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
+        var binding: RvItemConversationWithLastMessageBinding
+
+        init {
+            binding = RvItemConversationWithLastMessageBinding.bind(view!!)
+        }
+    }
+
+    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 - 21
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
@@ -64,9 +64,7 @@ import com.nextcloud.talk.jobs.SignalingSettingsWorker
 import com.nextcloud.talk.ui.theme.ThemeModule
 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
@@ -164,7 +162,6 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
         securityKeyManager.init(this, securityKeyConfig)
 
         initializeWebRtc()
-        DisplayUtils.useCompatVectorIfNeeded()
         buildComponent()
         DavUtils.registerCustomFactories()
 
@@ -174,18 +171,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 +225,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 +239,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;

+ 52 - 57
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
             )
@@ -1150,14 +1149,10 @@ class ChatController(args: Bundle) :
     }
 
     private fun isRecordAudioPermissionGranted(): Boolean {
-        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return PermissionChecker.checkSelfPermission(
-                context,
-                Manifest.permission.RECORD_AUDIO
-            ) == PermissionChecker.PERMISSION_GRANTED
-        } else {
-            true
-        }
+        return PermissionChecker.checkSelfPermission(
+            context,
+            Manifest.permission.RECORD_AUDIO
+        ) == PermissionChecker.PERMISSION_GRANTED
     }
 
     private fun startAudioRecording(file: String) {

+ 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 -> {

+ 52 - 79
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) {
@@ -585,15 +558,15 @@ class ConversationsListController(bundle: Bundle) :
         if (activity != null) {
             val conversationItem = ConversationItem(
                 conversation,
-                currentUser,
-                activity,
+                currentUser!!,
+                activity!!,
                 viewThemeUtils
             )
             conversationItems.add(conversationItem)
             val conversationItemWithHeader = ConversationItem(
                 conversation,
-                currentUser,
-                activity,
+                currentUser!!,
+                activity!!,
                 callHeaderItems[headerTitle],
                 viewThemeUtils
             )
@@ -651,8 +624,8 @@ class ConversationsListController(bundle: Bundle) :
                         }
                         val conversationItem = ConversationItem(
                             conversation,
-                            currentUser,
-                            activity,
+                            currentUser!!,
+                            activity!!,
                             callHeaderItems[headerTitle],
                             viewThemeUtils
                         )

+ 3 - 7
app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt

@@ -442,23 +442,19 @@ class LocationPickerController(args: Bundle) :
     private fun isLocationPermissionsGranted(): Boolean {
         fun isCoarseLocationGranted(): Boolean {
             return PermissionChecker.checkSelfPermission(
-                context!!,
+                context,
                 Manifest.permission.ACCESS_COARSE_LOCATION
             ) == PermissionChecker.PERMISSION_GRANTED
         }
 
         fun isFineLocationGranted(): Boolean {
             return PermissionChecker.checkSelfPermission(
-                context!!,
+                context,
                 Manifest.permission.ACCESS_FINE_LOCATION
             ) == PermissionChecker.PERMISSION_GRANTED
         }
 
-        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            isCoarseLocationGranted() && isFineLocationGranted()
-        } else {
-            true
-        }
+        return isCoarseLocationGranted() && isFineLocationGranted()
     }
 
     private fun requestLocationPermissions() {

+ 5 - 15
app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt

@@ -25,12 +25,10 @@ import android.app.Activity
 import android.app.KeyguardManager
 import android.content.Context
 import android.content.Intent
-import android.os.Build
 import android.os.Handler
 import android.os.Looper
 import android.util.Log
 import android.view.View
-import androidx.annotation.RequiresApi
 import androidx.biometric.BiometricPrompt
 import androidx.biometric.BiometricPrompt.PromptInfo
 import androidx.core.content.res.ResourcesCompat
@@ -62,15 +60,11 @@ class LockedController : BaseController(R.layout.controller_locked) {
     override fun onViewBound(view: View) {
         super.onViewBound(view)
         sharedApplication!!.componentApplication.inject(this)
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            binding.unlockContainer.setOnClickListener {
-                unlock()
-            }
+        binding.unlockContainer.setOnClickListener {
+            unlock()
         }
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     override fun onAttach(view: View) {
         super.onAttach(view)
         Log.d(TAG, "onAttach")
@@ -92,12 +86,10 @@ class LockedController : BaseController(R.layout.controller_locked) {
         Log.d(TAG, "onDetach")
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     fun unlock() {
         checkIfWeAreSecure()
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     private fun showBiometricDialog() {
         val context: Context? = activity
         if (context != null) {
@@ -140,11 +132,10 @@ class LockedController : BaseController(R.layout.controller_locked) {
         }
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     private fun checkIfWeAreSecure() {
         val keyguardManager = activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
-        if (keyguardManager?.isKeyguardSecure == true && appPreferences!!.isScreenLocked) {
-            if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences!!.screenLockTimeout)) {
+        if (keyguardManager?.isKeyguardSecure == true && appPreferences.isScreenLocked) {
+            if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) {
                 Log.d(TAG, "showBiometricDialog because 'we are NOT authenticated'...")
                 showBiometricDialog()
             } else {
@@ -172,8 +163,7 @@ class LockedController : BaseController(R.layout.controller_locked) {
         if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
             if (resultCode == Activity.RESULT_OK) {
                 if (
-                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
-                    SecurityUtils.checkIfWeAreAuthenticated(appPreferences!!.screenLockTimeout)
+                    SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)
                 ) {
                     Log.d(TAG, "All went well, dismiss locked controller")
                     router.popCurrentController()

+ 27 - 38
app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt

@@ -157,18 +157,13 @@ class SettingsController : BaseController(R.layout.controller_settings) {
             binding.settingsIncognitoKeyboard.visibility = View.GONE
         }
 
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            binding.settingsScreenLock.visibility = View.GONE
-            binding.settingsScreenLockTimeout.visibility = View.GONE
-        } else {
-            binding.settingsScreenLock.setSummary(
-                String.format(
-                    Locale.getDefault(),
-                    resources!!.getString(R.string.nc_settings_screen_lock_desc),
-                    resources!!.getString(R.string.nc_app_product_name)
-                )
+        binding.settingsScreenLock.setSummary(
+            String.format(
+                Locale.getDefault(),
+                resources!!.getString(R.string.nc_settings_screen_lock_desc),
+                resources!!.getString(R.string.nc_app_product_name)
             )
-        }
+        )
 
         setupPrivacyUrl()
         setupSourceCodeUrl()
@@ -662,10 +657,8 @@ class SettingsController : BaseController(R.layout.controller_settings) {
                 appPreferences.isKeyboardIncognito
         }
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            (binding.settingsIncognitoKeyboard.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
-                appPreferences.isKeyboardIncognito
-        }
+        (binding.settingsIncognitoKeyboard.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
+            appPreferences.isKeyboardIncognito
 
         if (CapabilitiesUtilNew.isReadStatusAvailable(userManager.currentUser.blockingGet())) {
             (binding.settingsReadPrivacy.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
@@ -679,29 +672,27 @@ class SettingsController : BaseController(R.layout.controller_settings) {
     }
 
     private fun setupScreenLockSetting() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
-            if (keyguardManager.isKeyguardSecure) {
-                binding.settingsScreenLock.isEnabled = true
-                binding.settingsScreenLockTimeout.isEnabled = true
-                (binding.settingsScreenLock.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
-                    appPreferences.isScreenLocked
-                binding.settingsScreenLockTimeout.isEnabled = appPreferences.isScreenLocked
-                if (appPreferences.isScreenLocked) {
-                    binding.settingsScreenLockTimeout.alpha = ENABLED_ALPHA
-                } else {
-                    binding.settingsScreenLockTimeout.alpha = DISABLED_ALPHA
-                }
-                binding.settingsScreenLock.alpha = ENABLED_ALPHA
+        val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+        if (keyguardManager.isKeyguardSecure) {
+            binding.settingsScreenLock.isEnabled = true
+            binding.settingsScreenLockTimeout.isEnabled = true
+            (binding.settingsScreenLock.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked =
+                appPreferences.isScreenLocked
+            binding.settingsScreenLockTimeout.isEnabled = appPreferences.isScreenLocked
+            if (appPreferences.isScreenLocked) {
+                binding.settingsScreenLockTimeout.alpha = ENABLED_ALPHA
             } else {
-                binding.settingsScreenLock.isEnabled = false
-                binding.settingsScreenLockTimeout.isEnabled = false
-                appPreferences.removeScreenLock()
-                appPreferences.removeScreenLockTimeout()
-                (binding.settingsScreenLock.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked = false
-                binding.settingsScreenLock.alpha = DISABLED_ALPHA
                 binding.settingsScreenLockTimeout.alpha = DISABLED_ALPHA
             }
+            binding.settingsScreenLock.alpha = ENABLED_ALPHA
+        } else {
+            binding.settingsScreenLock.isEnabled = false
+            binding.settingsScreenLockTimeout.isEnabled = false
+            appPreferences.removeScreenLock()
+            appPreferences.removeScreenLockTimeout()
+            (binding.settingsScreenLock.findViewById<View>(R.id.mp_checkable) as Checkable).isChecked = false
+            binding.settingsScreenLock.alpha = DISABLED_ALPHA
+            binding.settingsScreenLockTimeout.alpha = DISABLED_ALPHA
         }
     }
 
@@ -805,9 +796,7 @@ class SettingsController : BaseController(R.layout.controller_settings) {
 
     private inner class ScreenLockTimeoutListener : OnPreferenceValueChangedListener<String?> {
         override fun onChanged(newValue: String?) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                SecurityUtils.createKey(appPreferences.screenLockTimeout)
-            }
+            SecurityUtils.createKey(appPreferences.screenLockTimeout)
         }
     }
 

+ 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
+    }
+}

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

@@ -447,11 +447,9 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
                 EmojiCompat.get().process(pushMessage.text!!)
             )
         }
-        if (Build.VERSION.SDK_INT >= 23) {
-            // This method should exist since API 21, but some phones don't have it
-            // So as a safeguard, we don't use it until 23
-            notificationBuilder.color = context!!.resources.getColor(R.color.colorPrimary)
-        }
+
+        notificationBuilder.color = context!!.resources.getColor(R.color.colorPrimary)
+
         val notificationInfoBundle = Bundle()
         notificationInfoBundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
         // could be an ID or a TOKEN
@@ -526,7 +524,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))
     }
@@ -694,7 +692,6 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             .subscribe(object : Observer<ParticipantsOverall> {
                 override fun onSubscribe(d: Disposable) = Unit
 
-                @RequiresApi(Build.VERSION_CODES.M)
                 override fun onNext(participantsOverall: ParticipantsOverall) {
                     val participantList: List<Participant> = participantsOverall.ocs!!.data!!
                     hasParticipantsInCall = participantList.isNotEmpty()
@@ -726,7 +723,6 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
                     Log.e(TAG, "Error in getPeersForCall", e)
                 }
 
-                @RequiresApi(Build.VERSION_CODES.M)
                 override fun onComplete() {
 
                     if (isCallNotificationVisible) {
@@ -821,7 +817,6 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         return PendingIntent.getActivity(context, requestCode, intent, intentFlag)
     }
 
-    @RequiresApi(Build.VERSION_CODES.M)
     private fun isCallNotificationVisible(decryptedPushMessage: DecryptedPushMessage): Boolean {
         var isVisible = false
 

+ 2 - 8
app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt

@@ -295,7 +295,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
                         false
                     }
                 }
-                Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
+                else -> {
                     if (PermissionChecker.checkSelfPermission(
                             context,
                             Manifest.permission.WRITE_EXTERNAL_STORAGE
@@ -308,10 +308,6 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
                         false
                     }
                 }
-                else -> { // permission is automatically granted on sdk<23 upon installation
-                    Log.d(TAG, "Permission is granted")
-                    true
-                }
             }
         }
 
@@ -325,7 +321,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
                         REQUEST_PERMISSION
                     )
                 }
-                Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
+                else -> {
                     controller.requestPermissions(
                         arrayOf(
                             Manifest.permission.WRITE_EXTERNAL_STORAGE
@@ -333,8 +329,6 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
                         REQUEST_PERMISSION
                     )
                 }
-                else -> { // permission is automatically granted on sdk<23 upon installation
-                }
             }
         }
 

+ 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

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

@@ -47,6 +47,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
 import io.reactivex.Observer
+import io.reactivex.Single
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
@@ -159,19 +160,25 @@ class DirectReplyReceiver : BroadcastReceiver() {
             .extractMessagingStyleFromNotification(previousNotification)
 
         // Add reply
-        val avatarUrl = ApiUtils.getUrlForAvatar(currentUser.baseUrl, currentUser.userId, false)
-        val me = Person.Builder()
-            .setName(currentUser.displayName)
-            .setIcon(NotificationUtils.loadAvatarSync(avatarUrl))
-            .build()
-        val message = NotificationCompat.MessagingStyle.Message(reply, System.currentTimeMillis(), me)
-        previousStyle?.addMessage(message)
-
-        // Set the updated style
-        previousBuilder.setStyle(previousStyle)
-
-        // Update the active notification.
-        NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build())
+        Single.fromCallable {
+            val avatarUrl = ApiUtils.getUrlForAvatar(currentUser.baseUrl, currentUser.userId, false)
+            val me = Person.Builder()
+                .setName(currentUser.displayName)
+                .setIcon(NotificationUtils.loadAvatarSync(avatarUrl, context))
+                .build()
+            val message = NotificationCompat.MessagingStyle.Message(reply, System.currentTimeMillis(), me)
+            previousStyle?.addMessage(message)
+
+            // Set the updated style
+            previousBuilder.setStyle(previousStyle)
+
+            // Check if notification still exists
+            if (findActiveNotification(systemNotificationId!!) != null) {
+                NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build())
+            }
+        }
+            .subscribeOn(Schedulers.io())
+            .subscribe()
     }
 
     companion object {

+ 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
             }

+ 55 - 220
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>
  *
@@ -22,7 +24,6 @@
 
 package com.nextcloud.talk.utils;
 
-import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
@@ -33,11 +34,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,48 +48,26 @@ 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;
 
 import org.greenrobot.eventbus.EventBus;
 
-import java.lang.reflect.Constructor;
-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;
 
@@ -103,12 +78,16 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.annotation.XmlRes;
-import androidx.appcompat.widget.AppCompatDrawableManager;
 import androidx.core.content.ContextCompat;
 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 +98,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 +131,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 +141,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);
@@ -254,31 +150,6 @@ public class DisplayUtils {
         return px / context.getResources().getDisplayMetrics().density;
     }
 
-    // Solution inspired by https://stackoverflow.com/questions/34936590/why-isnt-my-vector-drawable-scaling-as-expected
-    public static void useCompatVectorIfNeeded() {
-        if (Build.VERSION.SDK_INT < 23) {
-            try {
-                @SuppressLint("RestrictedApi") AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
-                Class<?> inflateDelegateClass = Class.forName(
-                    "android.support.v7.widget.AppCompatDrawableManager$InflateDelegate");
-                Class<?> vdcInflateDelegateClass = Class.forName(
-                    "android.support.v7.widget.AppCompatDrawableManager$VdcInflateDelegate");
-
-                Constructor<?> constructor = vdcInflateDelegateClass.getDeclaredConstructor();
-                constructor.setAccessible(true);
-                Object vdcInflateDelegate = constructor.newInstance();
-
-                Class<?> args[] = {String.class, inflateDelegateClass};
-                Method addDelegate = AppCompatDrawableManager.class.getDeclaredMethod("addDelegate", args);
-                addDelegate.setAccessible(true);
-                addDelegate.invoke(drawableManager, "vector", vdcInflateDelegate);
-            } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
-                InvocationTargetException | IllegalAccessException e) {
-                Log.e(TAG, "Failed to use reflection to enable proper vector scaling");
-            }
-        }
-    }
-
     public static Drawable getTintedDrawable(Resources res, @DrawableRes int drawableResId, @ColorRes int colorResId) {
         Drawable drawable = ResourcesCompat.getDrawable(res, drawableResId, null);
 
@@ -305,10 +176,8 @@ public class DisplayUtils {
             viewThemeUtils.material.colorChipDrawable(context, chip);
         }
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            Configuration config = context.getResources().getConfiguration();
-            chip.setLayoutDirection(config.getLayoutDirection());
-        }
+        Configuration config = context.getResources().getConfiguration();
+        chip.setLayoutDirection(config.getLayoutDirection());
 
         int drawable;
 
@@ -335,33 +204,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;
@@ -481,24 +354,23 @@ public class DisplayUtils {
         Window window = activity.getWindow();
         boolean isLightTheme = lightTheme(color);
         if (window != null) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                View decor = window.getDecorView();
-                if (isLightTheme) {
-                    int systemUiFlagLightStatusBar;
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                        systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
-                            View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
-                    } else {
-                        systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
-                    }
-                    decor.setSystemUiVisibility(systemUiFlagLightStatusBar);
+
+            View decor = window.getDecorView();
+            if (isLightTheme) {
+                int systemUiFlagLightStatusBar;
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                    systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
+                        View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
                 } else {
-                    decor.setSystemUiVisibility(0);
+                    systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
                 }
-                window.setStatusBarColor(color);
-            } else if (isLightTheme) {
-                window.setStatusBarColor(Color.BLACK);
+                decor.setSystemUiVisibility(systemUiFlagLightStatusBar);
+            } else {
+                decor.setSystemUiVisibility(0);
             }
+            window.setStatusBarColor(color);
+        } else if (isLightTheme) {
+            window.setStatusBarColor(Color.BLACK);
         }
     }
 
@@ -575,7 +447,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 +455,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 +484,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 - 27
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,
@@ -215,7 +218,7 @@ object NotificationUtils {
             notification: Notification
         ) -> Unit
     ) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || conversationUser.id == -1L || context == null) {
+        if (conversationUser.id == -1L || context == null) {
             return
         }
 
@@ -320,28 +323,30 @@ 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)
+            .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);
-    }
-}

+ 0 - 5
app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java

@@ -21,7 +21,6 @@
 package com.nextcloud.talk.utils;
 
 import android.content.res.Resources;
-import android.os.Build;
 import android.security.keystore.KeyGenParameterSpec;
 import android.security.keystore.KeyPermanentlyInvalidatedException;
 import android.security.keystore.KeyProperties;
@@ -50,7 +49,6 @@ import javax.crypto.KeyGenerator;
 import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
 
-import androidx.annotation.RequiresApi;
 import androidx.biometric.BiometricPrompt;
 
 public class SecurityUtils {
@@ -60,7 +58,6 @@ public class SecurityUtils {
 
     private static BiometricPrompt.CryptoObject cryptoObject;
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     public static boolean checkIfWeAreAuthenticated(String screenLockTimeout) {
         try {
             KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
@@ -95,12 +92,10 @@ public class SecurityUtils {
         }
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     public static BiometricPrompt.CryptoObject getCryptoObject() {
         return cryptoObject;
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.M)
     public static void createKey(String validity) {
         try {
             KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");

+ 4 - 9
app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt

@@ -23,7 +23,6 @@ package com.nextcloud.talk.utils.permissions
 
 import android.Manifest
 import android.content.Context
-import android.os.Build
 import androidx.core.content.PermissionChecker
 import com.nextcloud.talk.BuildConfig
 
@@ -32,13 +31,9 @@ class PlatformPermissionUtilImpl(private val context: Context) : PlatformPermiss
         "${BuildConfig.APPLICATION_ID}.${BuildConfig.PERMISSION_LOCAL_BROADCAST}"
 
     override fun isCameraPermissionGranted(): Boolean {
-        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return PermissionChecker.checkSelfPermission(
-                context,
-                Manifest.permission.CAMERA
-            ) == PermissionChecker.PERMISSION_GRANTED
-        } else {
-            true
-        }
+        return PermissionChecker.checkSelfPermission(
+            context,
+            Manifest.permission.CAMERA
+        ) == PermissionChecker.PERMISSION_GRANTED
     }
 }

+ 6 - 66
app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt

@@ -8,12 +8,9 @@
 
 package com.nextcloud.talk.utils.ssl
 
-import android.os.Build
-import java.io.IOException
 import java.net.InetAddress
 import java.net.Socket
 import java.security.GeneralSecurityException
-import java.util.LinkedList
 import javax.net.ssl.KeyManager
 import javax.net.ssl.SSLContext
 import javax.net.ssl.SSLSocket
@@ -35,69 +32,12 @@ class SSLSocketFactoryCompat(
         var cipherSuites: Array<String>? = null
 
         init {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                // Since Android 6.0 (API level 23),
-                // - TLSv1.1 and TLSv1.2 is enabled by default
-                // - SSLv3 is disabled by default
-                // - all modern ciphers are activated by default
-                protocols = null
-                cipherSuites = null
-            } else {
-                val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket?
-                try {
-                    socket?.let {
-                        /* set reasonable protocol versions */
-                        // - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
-                        // - remove all SSL versions (especially SSLv3) because they're insecure now
-                        val _protocols = LinkedList<String>()
-                        for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) })
-                            _protocols += protocol
-                        protocols = _protocols.toTypedArray()
-
-                        /* set up reasonable cipher suites */
-                        val knownCiphers = arrayOf<String>(
-                            // TLS 1.2
-                            "TLS_RSA_WITH_AES_256_GCM_SHA384",
-                            "TLS_RSA_WITH_AES_128_GCM_SHA256",
-                            "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
-                            "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
-                            "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
-                            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
-                            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
-                            // maximum interoperability
-                            "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
-                            "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
-                            "TLS_RSA_WITH_AES_128_CBC_SHA",
-                            // additionally
-                            "TLS_RSA_WITH_AES_256_CBC_SHA",
-                            "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
-                            "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
-                            "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
-                            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
-                        )
-                        val availableCiphers = socket.supportedCipherSuites
-
-                        /* For maximum security, preferredCiphers should *replace* enabled ciphers (thus
-                         * disabling ciphers which are enabled by default, but have become unsecure), but for
-                         * the security level of DAVdroid and maximum compatibility, disabling of insecure
-                         * ciphers should be a server-side task */
-
-                        // for the final set of enabled ciphers, take the ciphers enabled by default, ...
-                        val _cipherSuites = LinkedList<String>()
-                        _cipherSuites.addAll(socket.enabledCipherSuites)
-                        // ... add explicitly allowed ciphers ...
-                        _cipherSuites.addAll(knownCiphers)
-                        // ... and keep only those which are actually available
-                        _cipherSuites.retainAll(availableCiphers)
-
-                        cipherSuites = _cipherSuites.toTypedArray()
-                    }
-                } catch (e: IOException) {
-                    // Exception is to be ignored
-                } finally {
-                    socket?.close() // doesn't implement Closeable on all supported Android versions
-                }
-            }
+            // Since Android 6.0 (API level 23),
+            // - TLSv1.1 and TLSv1.2 is enabled by default
+            // - SSLv3 is disabled by default
+            // - all modern ciphers are activated by default
+            protocols = null
+            cipherSuites = null
         }
     }
 

+ 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 {
 

+ 10 - 15
app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java

@@ -41,7 +41,6 @@ import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
-import android.os.Build;
 import android.util.Log;
 
 import com.nextcloud.talk.events.PeerConnectionEvent;
@@ -388,22 +387,18 @@ public class WebRtcAudioManager {
      */
     @Deprecated
     private boolean hasWiredHeadset() {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            return audioManager.isWiredHeadsetOn();
-        } else {
-            @SuppressLint("WrongConstant") final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
-            for (AudioDeviceInfo device : devices) {
-                final int type = device.getType();
-                if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
-                    Log.d(TAG, "hasWiredHeadset: found wired headset");
-                    return true;
-                } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
-                    Log.d(TAG, "hasWiredHeadset: found USB audio device");
-                    return true;
-                }
+        @SuppressLint("WrongConstant") final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+        for (AudioDeviceInfo device : devices) {
+            final int type = device.getType();
+            if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
+                Log.d(TAG, "hasWiredHeadset: found wired headset");
+                return true;
+            } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
+                Log.d(TAG, "hasWiredHeadset: found USB audio device");
+                return true;
             }
-            return false;
         }
+        return false;
     }
 
     public void updateAudioDeviceState() {

+ 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"

+ 4 - 5
app/src/main/res/layout/poll_result_voter_item.xml

@@ -17,22 +17,21 @@
   ~ You should have received a copy of the GNU General Public License
   ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   -->
-
 <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">
+    tools:background="@color/white"
+    tools:ignore="UseCompoundDrawables">
 
-    <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"

+ 0 - 2
app/src/main/res/values-night/colors.xml

@@ -75,7 +75,5 @@
     <color name="grey_200">#818181</color>
 
     <color name="dialog_background">#353535</color>
-    <color name="vote_dialog_background">#424242</color>
-
 
 </resources>

+ 0 - 3
app/src/main/res/values/colors.xml

@@ -105,8 +105,5 @@
     <!-- this is just a helper for status icon background because getting the background color of a dialog is not
     possible?! don't use this to set the background of dialogs -->
     <color name="dialog_background">#FFFFFF</color>
-    <color name="vote_dialog_background">#FFFFFF</color>
-
-
 
 </resources>

+ 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>

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 1 error and 112 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 112 warnings</span>