That fuck shit the fascists are using
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}