kopia lustrzana https://github.com/ryukoposting/Signal-Android
461 wiersze
17 KiB
Java
461 wiersze
17 KiB
Java
package org.thoughtcrime.securesms.components;
|
|
|
|
import android.Manifest;
|
|
import android.animation.Animator;
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.PorterDuffColorFilter;
|
|
import android.graphics.Rect;
|
|
import android.util.AttributeSet;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.ImageView;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.annotation.DrawableRes;
|
|
import androidx.annotation.LayoutRes;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
import androidx.constraintlayout.widget.ConstraintSet;
|
|
|
|
import com.airbnb.lottie.LottieAnimationView;
|
|
import com.airbnb.lottie.LottieProperty;
|
|
import com.airbnb.lottie.model.KeyPath;
|
|
|
|
import org.signal.core.util.concurrent.SignalExecutors;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
|
import org.thoughtcrime.securesms.util.DateUtils;
|
|
import org.thoughtcrime.securesms.util.Projection;
|
|
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
|
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
|
|
|
import java.util.Locale;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
public class ConversationItemFooter extends ConstraintLayout {
|
|
|
|
private TextView dateView;
|
|
private TextView simView;
|
|
private ExpirationTimerView timerView;
|
|
private ImageView insecureIndicatorView;
|
|
private DeliveryStatusView deliveryStatusView;
|
|
private boolean onlyShowSendingStatus;
|
|
private TextView audioDuration;
|
|
private LottieAnimationView revealDot;
|
|
private PlaybackSpeedToggleTextView playbackSpeedToggleTextView;
|
|
private boolean isOutgoing;
|
|
private boolean hasShrunkDate;
|
|
|
|
private OnTouchDelegateChangedListener onTouchDelegateChangedListener;
|
|
|
|
private final Rect speedToggleHitRect = new Rect();
|
|
private final int touchTargetSize = ViewUtil.dpToPx(48);
|
|
|
|
private long previousMessageId;
|
|
|
|
public ConversationItemFooter(Context context) {
|
|
super(context);
|
|
init(null);
|
|
}
|
|
|
|
public ConversationItemFooter(Context context, @Nullable AttributeSet attrs) {
|
|
super(context, attrs);
|
|
init(attrs);
|
|
}
|
|
|
|
public ConversationItemFooter(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
super(context, attrs, defStyleAttr);
|
|
init(attrs);
|
|
}
|
|
|
|
private void init(@Nullable AttributeSet attrs) {
|
|
final TypedArray typedArray;
|
|
if (attrs != null) {
|
|
typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
|
|
} else {
|
|
typedArray = null;
|
|
}
|
|
|
|
final @LayoutRes int contentId;
|
|
if (typedArray != null) {
|
|
int mode = typedArray.getInt(R.styleable.ConversationItemFooter_footer_mode, 0);
|
|
isOutgoing = mode == 0;
|
|
|
|
if (isOutgoing) {
|
|
contentId = R.layout.conversation_item_footer_outgoing;
|
|
} else {
|
|
contentId = R.layout.conversation_item_footer_incoming;
|
|
}
|
|
} else {
|
|
contentId = R.layout.conversation_item_footer_outgoing;
|
|
isOutgoing = true;
|
|
}
|
|
|
|
inflate(getContext(), contentId, this);
|
|
|
|
dateView = findViewById(R.id.footer_date);
|
|
simView = findViewById(R.id.footer_sim_info);
|
|
timerView = findViewById(R.id.footer_expiration_timer);
|
|
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
|
|
deliveryStatusView = findViewById(R.id.footer_delivery_status);
|
|
audioDuration = findViewById(R.id.footer_audio_duration);
|
|
revealDot = findViewById(R.id.footer_revealed_dot);
|
|
playbackSpeedToggleTextView = findViewById(R.id.footer_audio_playback_speed_toggle);
|
|
|
|
if (typedArray != null) {
|
|
setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
|
|
setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white)));
|
|
setRevealDotColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_reveal_dot_color, getResources().getColor(R.color.core_white)));
|
|
typedArray.recycle();
|
|
}
|
|
|
|
dateView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
|
if (oldLeft != left || oldRight != right) {
|
|
notifyTouchDelegateChanged(getPlaybackSpeedToggleTouchDelegateRect(), playbackSpeedToggleTextView);
|
|
}
|
|
});
|
|
}
|
|
|
|
public void setOnTouchDelegateChangedListener(@Nullable OnTouchDelegateChangedListener onTouchDelegateChangedListener) {
|
|
this.onTouchDelegateChangedListener = onTouchDelegateChangedListener;
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
timerView.stopAnimation();
|
|
}
|
|
|
|
public void setMessageRecord(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
|
presentDate(messageRecord, locale);
|
|
presentSimInfo(messageRecord);
|
|
presentTimer(messageRecord);
|
|
presentInsecureIndicator(messageRecord);
|
|
presentDeliveryStatus(messageRecord);
|
|
presentAudioDuration(messageRecord);
|
|
}
|
|
|
|
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
|
|
long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis));
|
|
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
|
}
|
|
|
|
public void setPlaybackSpeedListener(@Nullable PlaybackSpeedToggleTextView.PlaybackSpeedListener playbackSpeedListener) {
|
|
playbackSpeedToggleTextView.setPlaybackSpeedListener(playbackSpeedListener);
|
|
}
|
|
|
|
public void setAudioPlaybackSpeed(float playbackSpeed, boolean isPlaying) {
|
|
if (isPlaying) {
|
|
showPlaybackSpeedToggle();
|
|
} else {
|
|
hidePlaybackSpeedToggle();
|
|
}
|
|
|
|
playbackSpeedToggleTextView.setCurrentSpeed(playbackSpeed);
|
|
}
|
|
|
|
public void setTextColor(int color) {
|
|
dateView.setTextColor(color);
|
|
simView.setTextColor(color);
|
|
audioDuration.setTextColor(color);
|
|
}
|
|
|
|
public void setIconColor(int color) {
|
|
timerView.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
|
insecureIndicatorView.setColorFilter(color);
|
|
deliveryStatusView.setTint(color);
|
|
}
|
|
|
|
public void setRevealDotColor(int color) {
|
|
revealDot.addValueCallback(
|
|
new KeyPath("**"),
|
|
LottieProperty.COLOR_FILTER,
|
|
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
|
);
|
|
}
|
|
|
|
public void setOnlyShowSendingStatus(boolean onlyShowSending, MessageRecord messageRecord) {
|
|
this.onlyShowSendingStatus = onlyShowSending;
|
|
presentDeliveryStatus(messageRecord);
|
|
}
|
|
|
|
public void enableBubbleBackground(@DrawableRes int drawableRes, @Nullable Integer tint) {
|
|
setBackgroundResource(drawableRes);
|
|
|
|
if (tint != null) {
|
|
getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
|
} else {
|
|
getBackground().clearColorFilter();
|
|
}
|
|
}
|
|
|
|
public void disableBubbleBackground() {
|
|
setBackground(null);
|
|
}
|
|
|
|
public @Nullable Projection getProjection(@NonNull ViewGroup coordinateRoot) {
|
|
if (getVisibility() == VISIBLE) {
|
|
return Projection.relativeToParent(coordinateRoot, this, new Projection.Corners(ViewUtil.dpToPx(11)));
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public TextView getDateView() {
|
|
return dateView;
|
|
}
|
|
|
|
private void notifyTouchDelegateChanged(@NonNull Rect rect, @NonNull View touchDelegate) {
|
|
if (onTouchDelegateChangedListener != null) {
|
|
onTouchDelegateChangedListener.onTouchDelegateChanged(rect, touchDelegate);
|
|
}
|
|
}
|
|
|
|
private void showPlaybackSpeedToggle() {
|
|
if (hasShrunkDate) {
|
|
return;
|
|
}
|
|
|
|
hasShrunkDate = true;
|
|
|
|
playbackSpeedToggleTextView.animate()
|
|
.alpha(1f)
|
|
.scaleX(1f)
|
|
.scaleY(1f)
|
|
.setDuration(150L)
|
|
.setListener(new AnimationCompleteListener() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
playbackSpeedToggleTextView.setClickable(true);
|
|
}
|
|
});
|
|
|
|
if (isOutgoing) {
|
|
dateView.setMaxWidth(ViewUtil.dpToPx(32));
|
|
} else {
|
|
ConstraintSet constraintSet = new ConstraintSet();
|
|
constraintSet.clone(this);
|
|
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, ViewUtil.dpToPx(40));
|
|
constraintSet.applyTo(this);
|
|
}
|
|
}
|
|
|
|
private void hidePlaybackSpeedToggle() {
|
|
if (!hasShrunkDate) {
|
|
return;
|
|
}
|
|
|
|
hasShrunkDate = false;
|
|
|
|
playbackSpeedToggleTextView.animate()
|
|
.alpha(0f)
|
|
.scaleX(0.5f)
|
|
.scaleY(0.5f)
|
|
.setDuration(150L).setListener(new AnimationCompleteListener() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
playbackSpeedToggleTextView.setClickable(false);
|
|
playbackSpeedToggleTextView.clearRequestedSpeed();
|
|
}
|
|
});
|
|
|
|
if (isOutgoing) {
|
|
dateView.setMaxWidth(Integer.MAX_VALUE);
|
|
} else {
|
|
ConstraintSet constraintSet = new ConstraintSet();
|
|
constraintSet.clone(this);
|
|
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, -1);
|
|
constraintSet.applyTo(this);
|
|
}
|
|
}
|
|
|
|
private @NonNull Rect getPlaybackSpeedToggleTouchDelegateRect() {
|
|
playbackSpeedToggleTextView.getHitRect(speedToggleHitRect);
|
|
|
|
int widthOffset = (touchTargetSize - speedToggleHitRect.width()) / 2;
|
|
int heightOffset = (touchTargetSize - speedToggleHitRect.height()) / 2;
|
|
|
|
speedToggleHitRect.top -= heightOffset;
|
|
speedToggleHitRect.left -= widthOffset;
|
|
speedToggleHitRect.right += widthOffset;
|
|
speedToggleHitRect.bottom += heightOffset;
|
|
|
|
return speedToggleHitRect;
|
|
}
|
|
|
|
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
|
dateView.forceLayout();
|
|
if (messageRecord.isFailed()) {
|
|
int errorMsg;
|
|
if (messageRecord.hasFailedWithNetworkFailures()) {
|
|
errorMsg = R.string.ConversationItem_error_network_not_delivered;
|
|
} else if (messageRecord.getRecipient().isPushGroup() && messageRecord.isIdentityMismatchFailure()) {
|
|
errorMsg = R.string.ConversationItem_error_partially_not_delivered;
|
|
} else {
|
|
errorMsg = R.string.ConversationItem_error_not_sent_tap_for_details;
|
|
}
|
|
|
|
dateView.setText(errorMsg);
|
|
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
|
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
|
} else if (messageRecord.isRateLimited()) {
|
|
dateView.setText(R.string.ConversationItem_send_paused);
|
|
} else {
|
|
dateView.setText(DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
|
}
|
|
}
|
|
|
|
private void presentSimInfo(@NonNull MessageRecord messageRecord) {
|
|
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(getContext());
|
|
|
|
if (messageRecord.isPush() || messageRecord.getSubscriptionId() == -1 || !Permissions.hasAll(getContext(), Manifest.permission.READ_PHONE_STATE) || !subscriptionManager.isMultiSim()) {
|
|
simView.setVisibility(View.GONE);
|
|
} else {
|
|
Optional<SubscriptionInfoCompat> subscriptionInfo = subscriptionManager.getActiveSubscriptionInfo(messageRecord.getSubscriptionId());
|
|
|
|
if (subscriptionInfo.isPresent() && messageRecord.isOutgoing()) {
|
|
simView.setText(getContext().getString(R.string.ConversationItem_from_s, subscriptionInfo.get().getDisplayName()));
|
|
simView.setVisibility(View.VISIBLE);
|
|
} else if (subscriptionInfo.isPresent()) {
|
|
simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName()));
|
|
simView.setVisibility(View.VISIBLE);
|
|
} else {
|
|
simView.setVisibility(View.GONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
private void presentTimer(@NonNull final MessageRecord messageRecord) {
|
|
if (messageRecord.getExpiresIn() > 0 && !messageRecord.isPending()) {
|
|
this.timerView.setVisibility(View.VISIBLE);
|
|
this.timerView.setPercentComplete(0);
|
|
|
|
if (messageRecord.getExpireStarted() > 0) {
|
|
this.timerView.setExpirationTime(messageRecord.getExpireStarted(),
|
|
messageRecord.getExpiresIn());
|
|
this.timerView.startAnimation();
|
|
|
|
if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= System.currentTimeMillis()) {
|
|
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
|
|
}
|
|
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
|
|
SignalExecutors.BOUNDED.execute(() -> {
|
|
ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
|
|
long id = messageRecord.getId();
|
|
boolean mms = messageRecord.isMms();
|
|
|
|
if (mms) {
|
|
SignalDatabase.mms().markExpireStarted(id);
|
|
} else {
|
|
SignalDatabase.sms().markExpireStarted(id);
|
|
}
|
|
|
|
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
|
|
});
|
|
}
|
|
} else {
|
|
this.timerView.setVisibility(View.GONE);
|
|
}
|
|
}
|
|
|
|
private void presentInsecureIndicator(@NonNull MessageRecord messageRecord) {
|
|
insecureIndicatorView.setVisibility(messageRecord.isSecure() ? View.GONE : View.VISIBLE);
|
|
}
|
|
|
|
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
|
|
long newMessageId = buildMessageId(messageRecord);
|
|
|
|
if (previousMessageId == newMessageId && deliveryStatusView.isPending() && !messageRecord.isPending()) {
|
|
if (messageRecord.getRecipient().isGroup()) {
|
|
SignalLocalMetrics.GroupMessageSend.onUiUpdated(messageRecord.getId());
|
|
} else {
|
|
SignalLocalMetrics.IndividualMessageSend.onUiUpdated(messageRecord.getId());
|
|
}
|
|
}
|
|
|
|
previousMessageId = newMessageId;
|
|
|
|
|
|
if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback()) {
|
|
deliveryStatusView.setNone();
|
|
return;
|
|
}
|
|
|
|
if (onlyShowSendingStatus) {
|
|
if (messageRecord.isOutgoing() && messageRecord.isPending()) {
|
|
deliveryStatusView.setPending();
|
|
} else {
|
|
deliveryStatusView.setNone();
|
|
}
|
|
} else {
|
|
if (!messageRecord.isOutgoing()) {
|
|
deliveryStatusView.setNone();
|
|
} else if (messageRecord.isPending()) {
|
|
deliveryStatusView.setPending();
|
|
} else if (messageRecord.isRemoteRead()) {
|
|
deliveryStatusView.setRead();
|
|
} else if (messageRecord.isDelivered()) {
|
|
deliveryStatusView.setDelivered();
|
|
} else {
|
|
deliveryStatusView.setSent();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void presentAudioDuration(@NonNull MessageRecord messageRecord) {
|
|
if (messageRecord.isMms()) {
|
|
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
|
|
|
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
|
|
showAudioDurationViews();
|
|
|
|
if (messageRecord.getViewedReceiptCount() > 0 || (messageRecord.isOutgoing() && Objects.equals(messageRecord.getRecipient(), Recipient.self()))) {
|
|
revealDot.setProgress(1f);
|
|
} else {
|
|
revealDot.setProgress(0f);
|
|
}
|
|
} else {
|
|
hideAudioDurationViews();
|
|
}
|
|
} else {
|
|
hideAudioDurationViews();
|
|
}
|
|
}
|
|
|
|
private void showAudioDurationViews() {
|
|
audioDuration.setVisibility(View.VISIBLE);
|
|
revealDot.setVisibility(View.VISIBLE);
|
|
playbackSpeedToggleTextView.setVisibility(View.VISIBLE);
|
|
}
|
|
|
|
private void hideAudioDurationViews() {
|
|
audioDuration.setVisibility(View.GONE);
|
|
revealDot.setVisibility(View.GONE);
|
|
playbackSpeedToggleTextView.setVisibility(View.GONE);
|
|
}
|
|
|
|
private long buildMessageId(@NonNull MessageRecord record) {
|
|
return record.isMms() ? -record.getId() : record.getId();
|
|
}
|
|
|
|
public interface OnTouchDelegateChangedListener {
|
|
void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView);
|
|
}
|
|
}
|