That fuck shit the fascists are using
at master 478 lines 18 kB view raw
1package org.tm.archive.components; 2 3import android.Manifest; 4import android.animation.Animator; 5import android.annotation.SuppressLint; 6import android.content.Context; 7import android.content.res.TypedArray; 8import android.graphics.PorterDuff; 9import android.graphics.PorterDuffColorFilter; 10import android.graphics.Rect; 11import android.util.AttributeSet; 12import android.view.View; 13import android.view.ViewGroup; 14import android.widget.ImageView; 15import android.widget.TextView; 16 17import androidx.annotation.DrawableRes; 18import androidx.annotation.LayoutRes; 19import androidx.annotation.NonNull; 20import androidx.annotation.Nullable; 21import androidx.constraintlayout.widget.ConstraintLayout; 22import androidx.constraintlayout.widget.ConstraintSet; 23 24import com.airbnb.lottie.LottieAnimationView; 25import com.airbnb.lottie.LottieProperty; 26import com.airbnb.lottie.model.KeyPath; 27 28import org.signal.core.util.concurrent.SignalExecutors; 29import org.tm.archive.R; 30import org.tm.archive.animation.AnimationCompleteListener; 31import org.tm.archive.conversation.ConversationItemDisplayMode; 32import org.tm.archive.conversation.v2.computed.FormattedDate; 33import org.tm.archive.database.SignalDatabase; 34import org.tm.archive.database.model.MessageRecord; 35import org.tm.archive.database.model.MmsMessageRecord; 36import org.tm.archive.dependencies.ApplicationDependencies; 37import org.tm.archive.permissions.Permissions; 38import org.tm.archive.recipients.Recipient; 39import org.tm.archive.util.DateUtils; 40import org.tm.archive.util.MessageRecordUtil; 41import org.tm.archive.util.Projection; 42import org.tm.archive.util.SignalLocalMetrics; 43import org.tm.archive.util.ViewUtil; 44import org.tm.archive.util.dualsim.SubscriptionInfoCompat; 45import org.tm.archive.util.dualsim.SubscriptionManagerCompat; 46 47import java.util.Locale; 48import java.util.Objects; 49import java.util.Optional; 50import java.util.concurrent.TimeUnit; 51 52public class ConversationItemFooter extends ConstraintLayout { 53 54 private TextView dateView; 55 private TextView simView; 56 private ExpirationTimerView timerView; 57 private ImageView insecureIndicatorView; 58 private DeliveryStatusView deliveryStatusView; 59 private boolean onlyShowSendingStatus; 60 private TextView audioDuration; 61 private LottieAnimationView revealDot; 62 private PlaybackSpeedToggleTextView playbackSpeedToggleTextView; 63 private boolean isOutgoing; 64 private boolean hasShrunkDate; 65 66 private OnTouchDelegateChangedListener onTouchDelegateChangedListener; 67 68 private final Rect speedToggleHitRect = new Rect(); 69 private final int touchTargetSize = ViewUtil.dpToPx(48); 70 71 private long previousMessageId; 72 73 public ConversationItemFooter(Context context) { 74 super(context); 75 init(null); 76 } 77 78 public ConversationItemFooter(Context context, @Nullable AttributeSet attrs) { 79 super(context, attrs); 80 init(attrs); 81 } 82 83 public ConversationItemFooter(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 84 super(context, attrs, defStyleAttr); 85 init(attrs); 86 } 87 88 private void init(@Nullable AttributeSet attrs) { 89 final TypedArray typedArray; 90 if (attrs != null) { 91 typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0); 92 } else { 93 typedArray = null; 94 } 95 96 final @LayoutRes int contentId; 97 if (typedArray != null) { 98 int mode = typedArray.getInt(R.styleable.ConversationItemFooter_footer_mode, 0); 99 isOutgoing = mode == 0; 100 101 if (isOutgoing) { 102 contentId = R.layout.conversation_item_footer_outgoing; 103 } else { 104 contentId = R.layout.conversation_item_footer_incoming; 105 } 106 } else { 107 contentId = R.layout.conversation_item_footer_outgoing; 108 isOutgoing = true; 109 } 110 111 inflate(getContext(), contentId, this); 112 113 dateView = findViewById(R.id.footer_date); 114 simView = findViewById(R.id.footer_sim_info); 115 timerView = findViewById(R.id.footer_expiration_timer); 116 insecureIndicatorView = findViewById(R.id.footer_insecure_indicator); 117 deliveryStatusView = findViewById(R.id.footer_delivery_status); 118 audioDuration = findViewById(R.id.footer_audio_duration); 119 revealDot = findViewById(R.id.footer_revealed_dot); 120 playbackSpeedToggleTextView = findViewById(R.id.footer_audio_playback_speed_toggle); 121 122 if (typedArray != null) { 123 setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white))); 124 setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white))); 125 setRevealDotColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_reveal_dot_color, getResources().getColor(R.color.core_white))); 126 typedArray.recycle(); 127 } 128 129 dateView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 130 if (oldLeft != left || oldRight != right) { 131 notifyTouchDelegateChanged(getPlaybackSpeedToggleTouchDelegateRect(), playbackSpeedToggleTextView); 132 } 133 }); 134 } 135 136 public void setOnTouchDelegateChangedListener(@Nullable OnTouchDelegateChangedListener onTouchDelegateChangedListener) { 137 this.onTouchDelegateChangedListener = onTouchDelegateChangedListener; 138 } 139 140 @Override 141 protected void onDetachedFromWindow() { 142 super.onDetachedFromWindow(); 143 timerView.stopAnimation(); 144 } 145 146 public void setMessageRecord(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) { 147 presentDate(messageRecord, locale, displayMode); 148 presentSimInfo(messageRecord); 149 presentTimer(messageRecord); 150 presentInsecureIndicator(messageRecord); 151 presentDeliveryStatus(messageRecord); 152 presentAudioDuration(messageRecord); 153 } 154 155 public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) { 156 long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis)); 157 audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60)); 158 } 159 160 public void setPlaybackSpeedListener(@Nullable PlaybackSpeedToggleTextView.PlaybackSpeedListener playbackSpeedListener) { 161 playbackSpeedToggleTextView.setPlaybackSpeedListener(playbackSpeedListener); 162 } 163 164 public void setAudioPlaybackSpeed(float playbackSpeed, boolean isPlaying) { 165 if (isPlaying) { 166 showPlaybackSpeedToggle(); 167 } else { 168 hidePlaybackSpeedToggle(); 169 } 170 171 playbackSpeedToggleTextView.setCurrentSpeed(playbackSpeed); 172 } 173 174 public void setTextColor(int color) { 175 dateView.setTextColor(color); 176 simView.setTextColor(color); 177 audioDuration.setTextColor(color); 178 } 179 180 public void setIconColor(int color) { 181 timerView.setColorFilter(color, PorterDuff.Mode.SRC_IN); 182 insecureIndicatorView.setColorFilter(color); 183 deliveryStatusView.setTint(color); 184 } 185 186 public void setRevealDotColor(int color) { 187 revealDot.addValueCallback( 188 new KeyPath("**"), 189 LottieProperty.COLOR_FILTER, 190 frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) 191 ); 192 } 193 194 public void setOnlyShowSendingStatus(boolean onlyShowSending, MessageRecord messageRecord) { 195 this.onlyShowSendingStatus = onlyShowSending; 196 presentDeliveryStatus(messageRecord); 197 } 198 199 public void enableBubbleBackground(@DrawableRes int drawableRes, @Nullable Integer tint) { 200 setBackgroundResource(drawableRes); 201 202 if (tint != null) { 203 getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY); 204 } else { 205 getBackground().clearColorFilter(); 206 } 207 } 208 209 public void disableBubbleBackground() { 210 setBackground(null); 211 } 212 213 public @Nullable Projection getProjection(@NonNull ViewGroup coordinateRoot) { 214 if (getVisibility() == VISIBLE) { 215 return Projection.relativeToParent(coordinateRoot, this, new Projection.Corners(ViewUtil.dpToPx(11))); 216 } else { 217 return null; 218 } 219 } 220 221 public View getDateView() { 222 return dateView; 223 } 224 225 private void notifyTouchDelegateChanged(@NonNull Rect rect, @NonNull View touchDelegate) { 226 if (onTouchDelegateChangedListener != null) { 227 onTouchDelegateChangedListener.onTouchDelegateChanged(rect, touchDelegate); 228 } 229 } 230 231 private void showPlaybackSpeedToggle() { 232 if (hasShrunkDate) { 233 return; 234 } 235 236 hasShrunkDate = true; 237 238 playbackSpeedToggleTextView.animate() 239 .alpha(1f) 240 .scaleX(1f) 241 .scaleY(1f) 242 .setDuration(150L) 243 .setListener(new AnimationCompleteListener() { 244 @Override 245 public void onAnimationEnd(Animator animation) { 246 playbackSpeedToggleTextView.setClickable(true); 247 } 248 }); 249 250 if (isOutgoing) { 251 dateView.setMaxWidth(ViewUtil.dpToPx(32)); 252 } else { 253 ConstraintSet constraintSet = new ConstraintSet(); 254 constraintSet.clone(this); 255 constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, ViewUtil.dpToPx(40)); 256 constraintSet.applyTo(this); 257 } 258 } 259 260 private void hidePlaybackSpeedToggle() { 261 if (!hasShrunkDate) { 262 return; 263 } 264 265 hasShrunkDate = false; 266 267 playbackSpeedToggleTextView.animate() 268 .alpha(0f) 269 .scaleX(0.5f) 270 .scaleY(0.5f) 271 .setDuration(150L).setListener(new AnimationCompleteListener() { 272 @Override 273 public void onAnimationEnd(Animator animation) { 274 playbackSpeedToggleTextView.setClickable(false); 275 playbackSpeedToggleTextView.clearRequestedSpeed(); 276 } 277 }); 278 279 if (isOutgoing) { 280 dateView.setMaxWidth(Integer.MAX_VALUE); 281 } else { 282 ConstraintSet constraintSet = new ConstraintSet(); 283 constraintSet.clone(this); 284 constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, -1); 285 constraintSet.applyTo(this); 286 } 287 } 288 289 private @NonNull Rect getPlaybackSpeedToggleTouchDelegateRect() { 290 playbackSpeedToggleTextView.getHitRect(speedToggleHitRect); 291 292 int widthOffset = (touchTargetSize - speedToggleHitRect.width()) / 2; 293 int heightOffset = (touchTargetSize - speedToggleHitRect.height()) / 2; 294 295 speedToggleHitRect.top -= heightOffset; 296 speedToggleHitRect.left -= widthOffset; 297 speedToggleHitRect.right += widthOffset; 298 speedToggleHitRect.bottom += heightOffset; 299 300 return speedToggleHitRect; 301 } 302 303 private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) { 304 dateView.forceLayout(); 305 if (messageRecord.isMediaPending()) { 306 dateView.setText(null); 307 } else if (messageRecord.isFailed()) { 308 int errorMsg; 309 if (messageRecord.hasFailedWithNetworkFailures()) { 310 errorMsg = R.string.ConversationItem_error_network_not_delivered; 311 } else if (messageRecord.getToRecipient().isPushGroup() && messageRecord.isIdentityMismatchFailure()) { 312 errorMsg = R.string.ConversationItem_error_partially_not_delivered; 313 } else { 314 errorMsg = R.string.ConversationItem_error_not_sent_tap_for_details; 315 } 316 317 dateView.setText(errorMsg); 318 } else if (messageRecord.isPendingInsecureSmsFallback()) { 319 dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted); 320 } else if (messageRecord.isRateLimited()) { 321 dateView.setText(R.string.ConversationItem_send_paused); 322 } else if (MessageRecordUtil.isScheduled(messageRecord)) { 323 dateView.setText(DateUtils.getOnlyTimeString(getContext(), ((MmsMessageRecord) messageRecord).getScheduledDate())); 324 } else { 325 long timestamp = messageRecord.getTimestamp(); 326 if (messageRecord.isEditMessage()) { 327 if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) { 328 timestamp = messageRecord.getDateSent(); 329 } 330 } 331 FormattedDate date = DateUtils.getDatelessRelativeTimeSpanFormattedDate(getContext(), locale, timestamp); 332 String dateLabel = date.getValue(); 333 if (displayMode != ConversationItemDisplayMode.Detailed.INSTANCE && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) { 334 if (date.isNow()) { 335 dateLabel = getContext().getString(R.string.ConversationItem_edited_now_timestamp_footer); 336 } else if (date.isRelative()) { 337 dateLabel = getContext().getString(R.string.ConversationItem_edited_relative_timestamp_footer, date.getValue()); 338 } else { 339 dateLabel = getContext().getString(R.string.ConversationItem_edited_absolute_timestamp_footer, date.getValue()); 340 } 341 } 342 dateView.setText(dateLabel); 343 } 344 } 345 346 private void presentSimInfo(@NonNull MessageRecord messageRecord) { 347 SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(getContext()); 348 349 if (messageRecord.isPush() || messageRecord.getSubscriptionId() == -1 || !Permissions.hasAll(getContext(), Manifest.permission.READ_PHONE_STATE) || !subscriptionManager.isMultiSim()) { 350 simView.setVisibility(View.GONE); 351 } else { 352 Optional<SubscriptionInfoCompat> subscriptionInfo = subscriptionManager.getActiveSubscriptionInfo(messageRecord.getSubscriptionId()); 353 354 if (subscriptionInfo.isPresent() && messageRecord.isOutgoing()) { 355 simView.setText(getContext().getString(R.string.ConversationItem_from_s, subscriptionInfo.get().getDisplayName())); 356 simView.setVisibility(View.VISIBLE); 357 } else if (subscriptionInfo.isPresent()) { 358 simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName())); 359 simView.setVisibility(View.VISIBLE); 360 } else { 361 simView.setVisibility(View.GONE); 362 } 363 } 364 } 365 366 @SuppressLint("StaticFieldLeak") 367 private void presentTimer(@NonNull final MessageRecord messageRecord) { 368 if (messageRecord.getExpiresIn() > 0 && !messageRecord.isPending()) { 369 this.timerView.setVisibility(View.VISIBLE); 370 this.timerView.setPercentComplete(0); 371 372 if (messageRecord.getExpireStarted() > 0) { 373 this.timerView.setExpirationTime(messageRecord.getExpireStarted(), 374 messageRecord.getExpiresIn()); 375 this.timerView.startAnimation(); 376 377 if (messageRecord.getExpireStarted() + messageRecord.getExpiresIn() <= System.currentTimeMillis()) { 378 ApplicationDependencies.getExpiringMessageManager().checkSchedule(); 379 } 380 } else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) { 381 SignalExecutors.BOUNDED.execute(() -> { 382 long id = messageRecord.getId(); 383 boolean mms = messageRecord.isMms(); 384 long now = System.currentTimeMillis(); 385 386 SignalDatabase.messages().markExpireStarted(id, now); 387 ApplicationDependencies.getExpiringMessageManager().scheduleDeletion(id, mms, now, messageRecord.getExpiresIn()); 388 }); 389 } 390 } else { 391 this.timerView.setVisibility(View.GONE); 392 } 393 } 394 395 private void presentInsecureIndicator(@NonNull MessageRecord messageRecord) { 396 insecureIndicatorView.setVisibility(messageRecord.isSecure() ? View.GONE : View.VISIBLE); 397 } 398 399 private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) { 400 long newMessageId = buildMessageId(messageRecord); 401 402 if (previousMessageId == newMessageId && deliveryStatusView.isPending() && !messageRecord.isPending()) { 403 if (messageRecord.getToRecipient().isGroup()) { 404 SignalLocalMetrics.GroupMessageSend.onUiUpdated(messageRecord.getId()); 405 } else { 406 SignalLocalMetrics.IndividualMessageSend.onUiUpdated(messageRecord.getId()); 407 } 408 } 409 410 previousMessageId = newMessageId; 411 412 413 if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback() || MessageRecordUtil.isScheduled(messageRecord)) { 414 deliveryStatusView.setNone(); 415 return; 416 } 417 418 if (onlyShowSendingStatus) { 419 if (messageRecord.isOutgoing() && messageRecord.isPending()) { 420 deliveryStatusView.setPending(); 421 } else { 422 deliveryStatusView.setNone(); 423 } 424 } else { 425 if (!messageRecord.isOutgoing()) { 426 deliveryStatusView.setNone(); 427 } else if (messageRecord.isPending()) { 428 deliveryStatusView.setPending(); 429 } else if (messageRecord.hasReadReceipt()) { 430 deliveryStatusView.setRead(); 431 } else if (messageRecord.isDelivered()) { 432 deliveryStatusView.setDelivered(); 433 } else { 434 deliveryStatusView.setSent(); 435 } 436 } 437 } 438 439 private void presentAudioDuration(@NonNull MessageRecord messageRecord) { 440 if (messageRecord.isMms()) { 441 MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord; 442 443 if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) { 444 showAudioDurationViews(); 445 446 if (messageRecord.isViewed() || (messageRecord.isOutgoing() && Objects.equals(messageRecord.getToRecipient(), Recipient.self()))) { 447 revealDot.setProgress(1f); 448 } else { 449 revealDot.setProgress(0f); 450 } 451 } else { 452 hideAudioDurationViews(); 453 } 454 } else { 455 hideAudioDurationViews(); 456 } 457 } 458 459 private void showAudioDurationViews() { 460 audioDuration.setVisibility(View.VISIBLE); 461 revealDot.setVisibility(View.VISIBLE); 462 playbackSpeedToggleTextView.setVisibility(View.VISIBLE); 463 } 464 465 private void hideAudioDurationViews() { 466 audioDuration.setVisibility(View.GONE); 467 revealDot.setVisibility(View.GONE); 468 playbackSpeedToggleTextView.setVisibility(View.GONE); 469 } 470 471 private long buildMessageId(@NonNull MessageRecord record) { 472 return record.isMms() ? -record.getId() : record.getId(); 473 } 474 475 public interface OnTouchDelegateChangedListener { 476 void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView); 477 } 478}