diff --git a/app/build.gradle b/app/build.gradle index 895f5f78c..db6336453 100644 --- a/app/build.gradle +++ b/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}" diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt index 538379357..a9e76d42a 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt +++ b/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) } } diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index cbd377bc9..cd667b435 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/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); diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt index f81389f67..6a6ed114e 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt +++ b/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>) { - Log.e(TAG, "failed to load avatar") - } - }, - UiThreadImmediateExecutorService.getInstance() - ) - } - private fun endMediaNotifications() { if (mediaPlayer != null) { if (mediaPlayer!!.isPlaying) { diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 9938c3dd5..f8055e66d 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/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) { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java index bd09717d6..b8b5cc60b 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java +++ b/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); diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt index 0a3ff60f7..551e15a26 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt +++ b/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) { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt index facc1cd49..2f9071ae0 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt +++ b/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 + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java index ad4179e88..36f652da1 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java +++ b/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= 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 diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java deleted file mode 100644 index a37396189..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * @author Marcel Hibbe - * Copyright (C) 2022 Marcel Hibbe - * Copyright (C) 2021 Andy Scherzinger - * Copyright (C) 2017-2018 Mario Danic - * - * 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 . - */ - -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 implements - ISectionable, IFilterable { - - 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 adapter) { - return new ConversationItemViewHolder(view, adapter); - } - - @SuppressLint("SetTextI18n") - @Override - public void bindViewHolder(FlexibleAdapter adapter, - ConversationItemViewHolder holder, - int position, - List 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); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt new file mode 100644 index 000000000..4b16ed6af --- /dev/null +++ b/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 + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2017-2018 Mario Danic + * + * 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 . + */ +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(), + ISectionable, + IFilterable { + 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?>?): ConversationItemViewHolder { + return ConversationItemViewHolder(view, adapter) + } + + @SuppressLint("SetTextI18n") + override fun bindViewHolder( + adapter: FlexibleAdapter?>, + holder: ConversationItemViewHolder, + position: Int, + payloads: List + ) { + 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java index 1773e1c60..13c655ff7 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java +++ b/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= 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); diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt index d00df59d4..fe2424125 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt +++ b/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 * 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 { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java index ea676457b..6bc572d68 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java +++ b/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= 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= Build.VERSION_CODES.O) { - val layers = arrayOfNulls(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() } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt index 8da197c1d..65a4bc8f6 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt +++ b/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(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) { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt index 5466c5a8e..c1b9a00ff 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt +++ b/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(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() } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java index a60f089f1..78fbac71e 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java +++ b/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 + * Copyright (C) 2021-2022 Tim Krüger * Copyright (C) 2021 Andy Scherzinger * * 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; } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt index 81b26b99a..800375c6a 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt +++ b/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(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() } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index 2cc1419aa..ab67e99d6 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/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(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() diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt index 5a80b08ef..f3da8ee2d 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt +++ b/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 } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java index f6860c9da..82778e9dd 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java +++ b/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; } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index 7891e601b..cfd4be17b 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/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 + * Copyright (C) 2021-2022 Tim Krüger * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2021 Marcel Hibbe * Copyright (C) 2017-2018 Mario Danic @@ -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(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? diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index 5407a12f4..de58edb28 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/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 * Copyright (C) 2022 Marcel Hibbe * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2017 Mario Danic @@ -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 { diff --git a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java index eeeaa45e0..07e733e26 100644 --- a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java +++ b/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; diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index a94d7ca03..1648429dd 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/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 + * Copyright (C) 2021-2022 Tim Krüger * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2021-2022 Marcel Hibbe * Copyright (C) 2017-2019 Mario Danic @@ -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) + private fun setIcon(drawable: Drawable?) { - 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) + actionBar?.let { + val avatarSize = (it.height / TOOLBAR_AVATAR_RATIO).roundToInt() - val roundedBitmapDrawable = - RoundedBitmapDrawableFactory.create(resources!!, bitmapResized) - roundedBitmapDrawable.isCircular = true - roundedBitmapDrawable.setAntiAlias(true) - actionBar?.setIcon(roundedBitmapDrawable) - } else { - Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0") - } + 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>) { - // 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) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index 84930b8ee..a44f16f4b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/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(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 -> { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt index ae1d5b4ae..4d6fafc18 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt +++ b/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) { - 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) - button.icon = roundedBitmapDrawable - } - } + private fun loadUserAvatar( + target: Target + ) { - override fun onFailureImpl(dataSource: DataSource>) { - if (resources != null) { - button.icon = ResourcesCompat.getDrawable(resources!!, R.drawable.ic_user, null) - } - } - }, - UiThreadImmediateExecutorService.getInstance() + if (activity != null) { + val url = ApiUtils.getUrlForAvatar( + currentUser!!.baseUrl, + currentUser!!.userId, + true + ) + + 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>) { - 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 ) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt b/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt index 714e2fa4b..a440fb452 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt +++ b/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() { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt b/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt index e18bdbbd6..43f39435f 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt +++ b/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() diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt index b288900ee..95adb858f 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt +++ b/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(R.id.mp_checkable) as Checkable).isChecked = - appPreferences.isKeyboardIncognito - } + (binding.settingsIncognitoKeyboard.findViewById(R.id.mp_checkable) as Checkable).isChecked = + appPreferences.isKeyboardIncognito if (CapabilitiesUtilNew.isReadStatusAvailable(userManager.currentUser.blockingGet())) { (binding.settingsReadPrivacy.findViewById(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(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(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(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(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 { override fun onChanged(newValue: String?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - SecurityUtils.createKey(appPreferences.screenLockTimeout) - } + SecurityUtils.createKey(appPreferences.screenLockTimeout) } } diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt new file mode 100644 index 000000000..c8b391d22 --- /dev/null +++ b/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 + * 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 . + */ + +@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(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(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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 2d7dc11b7..3f02ba7e9 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/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 { override fun onSubscribe(d: Disposable) = Unit - @RequiresApi(Build.VERSION_CODES.M) override fun onNext(participantsOverall: ParticipantsOverall) { val participantList: List = 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 diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index 5be620dd3..47ca9e8d8 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/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 - } } } diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt index e69f0038b..bc415f4e2 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt +++ b/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 } } diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt index d071cc76b..d934115fb 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt +++ b/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 * Copyright (C) 2022 Marcel Hibbe * * 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 diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt index 64f62f564..feec10aea 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt +++ b/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) + 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) + // Set the updated style + previousBuilder.setStyle(previousStyle) - // Update the active notification. - NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build()) + // Check if notification still exists + if (findActiveNotification(systemNotificationId!!) != null) { + NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build()) + } + } + .subscribeOn(Schedulers.io()) + .subscribe() } companion object { diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt index bef8be657..b1e10a118 100644 --- a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt index fa76dcccb..027b849aa 100644 --- a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt +++ b/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)) } } diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt index 30977e6c8..d2b0cb7ad 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt index f63549ede..1eb23cf90 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt index a1861cdaa..c699e3595 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt +++ b/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 = object : BaseControllerListener() { - 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) {} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java index c42b711b3..d75c99403 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java +++ b/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); } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt index 90bdba9dd..38a90a99b 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt +++ b/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 } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index cb363337d..a4ab68ed7 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/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 * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2017-2020 Mario Danic * @@ -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 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> 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))); + public void onStart(@Nullable Drawable drawable) { - // A hack to refresh the chip icon - if (emojiEditText != null) { - emojiEditText.post(() -> emojiEditText.setTextKeepState( - emojiEditText.getText(), - TextView.BufferType.SPANNABLE)); - } + } + + @Override + public void onError(@Nullable Drawable drawable) { + + } + + @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(); - @Override - protected void onFailureImpl(DataSource> dataSource) { - } - }, - UiThreadImmediateExecutorService.getInstance()); + 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 */ diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt index 2b4f3524a..487c4f3c7 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt +++ b/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 { - private val alphanumComparator = AlphanumComparator() + private val alphanumComparator = + AlphanumComparator() override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int { return if (!left.isFile && !right.isFile) { diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt index 35d1f2998..098c2dceb 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt +++ b/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( diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index 62bbc837c..7ada7b797 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/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? - 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 } diff --git a/app/src/main/java/com/nextcloud/talk/utils/OkHttpNetworkFetcherWithCache.java b/app/src/main/java/com/nextcloud/talk/utils/OkHttpNetworkFetcherWithCache.java deleted file mode 100644 index 82e483cf0..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/OkHttpNetworkFetcherWithCache.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 Mario Danic - * - * 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 . - */ - -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); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java b/app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java index 5420f44af..50f404eff 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java +++ b/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"); diff --git a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt index 5f2b65976..27f4a6f71 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt +++ b/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 } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt index b50113882..071226037 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt +++ b/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? = 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() - for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) }) - _protocols += protocol - protocols = _protocols.toTypedArray() - - /* set up reasonable cipher suites */ - val knownCiphers = arrayOf( - // 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() - _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 } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java b/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java index 077ebe857..50d6bfa07 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java +++ b/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 { diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java index 45c97cac3..dd0818a4f 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java +++ b/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() { diff --git a/app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java b/app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java similarity index 99% rename from app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java rename to app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java index 35cd76177..59eac62ea 100644 --- a/app/src/main/java/third_parties/daveKoeller/AlphanumComparator.java +++ b/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; diff --git a/app/src/main/java/third_parties/daveKoeller/lgpl-2.1.txt b/app/src/main/java/third/parties/daveKoeller/lgpl-2.1.txt similarity index 100% rename from app/src/main/java/third_parties/daveKoeller/lgpl-2.1.txt rename to app/src/main/java/third/parties/daveKoeller/lgpl-2.1.txt diff --git a/app/src/main/java/third/parties/fresco/BetterImageSpan.kt b/app/src/main/java/third/parties/fresco/BetterImageSpan.kt new file mode 100644 index 000000000..fbd003ed0 --- /dev/null +++ b/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 + } +} diff --git a/app/src/main/res/drawable/shape_oval.xml b/app/src/main/res/drawable/shape_oval.xml new file mode 100644 index 000000000..df8bfd8bd --- /dev/null +++ b/app/src/main/res/drawable/shape_oval.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/account_item.xml b/app/src/main/res/layout/account_item.xml index 1f4b04761..8668dbcb9 100644 --- a/app/src/main/res/layout/account_item.xml +++ b/app/src/main/res/layout/account_item.xml @@ -21,7 +21,6 @@ --> - + android:src="@drawable/account_circle_48dp" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 55fab6b8a..430439fdb 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/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" /> diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index e7dc38665..2a3f3b71b 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/app/src/main/res/layout/call_activity.xml @@ -2,9 +2,11 @@ ~ Nextcloud Talk application ~ ~ @author Mario Danic - ~ Copyright (C) 2017-2018 Mario Danic ~ @author Marcel Hibbe + ~ @author Tim Krüger + ~ Copyright (C) 2022 Tim Krüger ~ Copyright (C) 2021 Marcel Hibbe + ~ Copyright (C) 2017-2018 Mario Danic ~ ~ 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" /> - + app:srcCompat="@drawable/ic_switch_video_white_24px" + android:contentDescription="@string/nc_call_button_content_description_switch_to_self_vide"/> - + 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" /> - + 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" /> - + 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" /> - + 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" /> - + 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" /> - + app:srcCompat="@drawable/ic_circular_group" + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/call_item.xml b/app/src/main/res/layout/call_item.xml index 41f175472..8b87a7eac 100644 --- a/app/src/main/res/layout/call_item.xml +++ b/app/src/main/res/layout/call_item.xml @@ -31,12 +31,12 @@ android:gravity="center" android:orientation="vertical"> - + android:contentDescription="@string/avatar"/> ~ Copyright (C) 2021 Andy Scherzinger ~ Copyright (C) 2017-2018 Mario Danic ~ @@ -36,36 +38,37 @@ android:animateLayoutChanges="true" android:orientation="horizontal"> - + 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" /> - + 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" /> - + tools:visibility="visible" + android:contentDescription="@string/nc_call_button_content_description_answer_video_call" /> - + android:contentDescription="@string/avatar" /> diff --git a/app/src/main/res/layout/controller_conversation_info.xml b/app/src/main/res/layout/controller_conversation_info.xml index 6ae406ad2..53d8fd534 100644 --- a/app/src/main/res/layout/controller_conversation_info.xml +++ b/app/src/main/res/layout/controller_conversation_info.xml @@ -77,13 +77,13 @@ android:layout_marginTop="@dimen/margin_between_elements" tools:text="Jane Doe" /> - + tools:background="@color/hwSecurityRed" + android:contentDescription="@string/avatar" /> diff --git a/app/src/main/res/layout/controller_profile.xml b/app/src/main/res/layout/controller_profile.xml index d09b65b7d..f401adaf3 100644 --- a/app/src/main/res/layout/controller_profile.xml +++ b/app/src/main/res/layout/controller_profile.xml @@ -19,7 +19,6 @@ --> - + android:contentDescription="@string/avatar" /> - + android:contentDescription="@string/avatar" /> - + android:src="@drawable/account_circle_48dp" /> - + android:contentDescription="@string/avatar" /> - + android:contentDescription="@string/avatar" /> - + android:contentDescription="@string/avatar" /> - + android:contentDescription="@string/avatar" /> - - + tools:src="@drawable/ic_call_black_24dp" + tools:ignore="ContentDescription" /> - - + tools:src="@drawable/ic_call_black_24dp" + tools:ignore="ContentDescription" /> - + android:contentDescription="@string/avatar" /> - + android:contentDescription="@string/avatar" /> - - + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:scaleType="fitStart" + tools:src="@drawable/ic_call_black_24dp" + tools:ignore="ContentDescription" /> - - + tools:src="@drawable/ic_call_black_24dp" + tools:ignore="ContentDescription" /> . --> - + tools:background="@color/white" + tools:ignore="UseCompoundDrawables"> - + android:contentDescription="@string/avatar"/> - + android:contentDescription="@string/avatar"/> + tools:layout_height="100dp" /> + tools:visibility="visible" /> + tools:visibility="visible" /> + tools:visibility="visible" /> - + tools:visibility="visible" + tools:ignore="ContentDescription" /> \ No newline at end of file diff --git a/app/src/main/res/layout/rv_item_browser_file.xml b/app/src/main/res/layout/rv_item_browser_file.xml index 08958cdff..74bf7c065 100644 --- a/app/src/main/res/layout/rv_item_browser_file.xml +++ b/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" /> - + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/rv_item_contact.xml b/app/src/main/res/layout/rv_item_contact.xml index 7bcefd7e7..4abb3ecee 100644 --- a/app/src/main/res/layout/rv_item_contact.xml +++ b/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" /> - + android:contentDescription="@string/avatar" /> diff --git a/app/src/main/res/layout/rv_item_contact_shimmer.xml b/app/src/main/res/layout/rv_item_contact_shimmer.xml index d392feb5a..417efe7bd 100644 --- a/app/src/main/res/layout/rv_item_contact_shimmer.xml +++ b/app/src/main/res/layout/rv_item_contact_shimmer.xml @@ -29,7 +29,7 @@ android:orientation="vertical"> diff --git a/app/src/main/res/layout/rv_item_conversation_info_participant.xml b/app/src/main/res/layout/rv_item_conversation_info_participant.xml index c72f1b1d3..494a1c608 100644 --- a/app/src/main/res/layout/rv_item_conversation_info_participant.xml +++ b/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"> - + app:layout_constraintTop_toTopOf="parent" /> ~ Copyright (C) 2021 Andy Scherzinger ~ Copyright (C) 2017-2018 Mario Danic ~ @@ -38,12 +40,11 @@ android:layout_centerVertical="true" android:layout_marginEnd="@dimen/double_margin_between_elements"> - + android:contentDescription="@null" /> + app:layout_constraintTop_toTopOf="parent" /> ~ Copyright (C) 2021 Andy Scherzinger ~ Copyright (C) 2017-2018 Mario Danic ~ @@ -35,14 +37,13 @@ android:layout_margin="@dimen/double_margin_between_elements" tools:background="@color/white"> - + app:layout_constraintTop_toTopOf="parent" /> @@ -40,14 +40,12 @@ app:layout_flexGrow="1" app:layout_wrapBefore="true"> - + tools:ignore="ContentDescription" /> - + tools:src="@drawable/ic_call_black_24dp" + tools:ignore="ContentDescription" /> #818181 #353535 - #424242 - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 62b316e58..741d89889 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -105,8 +105,5 @@ #FFFFFF - #FFFFFF - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b65ccdce3..78536e983 100644 --- a/app/src/main/res/values/dimens.xml +++ b/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 ~ Copyright (C) 2017-2019 Mario Danic ~ ~ This program is free software: you can redistribute it and/or modify @@ -67,6 +69,8 @@ 80dp 180dp 110dp + 10dp + 10dp 48dp 48dp 0dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fa3b5a2c..40ec2036e 100644 --- a/app/src/main/res/values/strings.xml +++ b/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 ~ Copyright (C) 2022 Marcel Hibbe ~ Copyright (C) 2021 Andy Scherzinger ~ Copyright (C) 2017-2018 Mario Danic @@ -211,6 +213,14 @@ %1$s with phone %1$s with video You missed a call from %s + Open picture in picture mode + Change audio output + Toggle camera + Toggle microphone + Hangup + Answer as voice call only + Answer as video call + Switch to self video Mute microphone diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 02dd5476d..e0e3dde3a 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 1 error and 112 warnings + Lint Report: 112 warnings