That fuck shit the fascists are using
at master 517 lines 20 kB view raw
1package org.tm.archive.components; 2 3 4import android.content.Context; 5import android.content.res.TypedArray; 6import android.graphics.Canvas; 7import android.text.TextUtils; 8import android.util.AttributeSet; 9import android.view.View; 10import android.view.ViewGroup; 11import android.widget.ImageView; 12import android.widget.TextView; 13 14import androidx.annotation.NonNull; 15import androidx.annotation.Nullable; 16import androidx.constraintlayout.widget.ConstraintLayout; 17 18import com.bumptech.glide.RequestManager; 19import com.bumptech.glide.load.engine.DiskCacheStrategy; 20import com.google.android.material.imageview.ShapeableImageView; 21import com.google.android.material.shape.CornerFamily; 22import com.google.android.material.shape.ShapeAppearanceModel; 23 24import org.signal.core.util.DimensionUnit; 25import org.signal.core.util.logging.Log; 26import org.tm.archive.R; 27import org.tm.archive.attachments.Attachment; 28import org.tm.archive.components.emoji.EmojiImageView; 29import org.tm.archive.components.emoji.EmojiTextView; 30import org.tm.archive.components.mention.MentionAnnotation; 31import org.tm.archive.components.quotes.QuoteViewColorTheme; 32import org.tm.archive.conversation.MessageStyler; 33import org.tm.archive.database.model.Mention; 34import org.tm.archive.database.model.databaseprotos.BodyRangeList; 35import org.tm.archive.mms.DecryptableStreamUriLoader.DecryptableUri; 36import org.tm.archive.mms.QuoteModel; 37import org.tm.archive.mms.Slide; 38import org.tm.archive.mms.SlideDeck; 39import org.tm.archive.recipients.LiveRecipient; 40import org.tm.archive.recipients.Recipient; 41import org.tm.archive.recipients.RecipientForeverObserver; 42import org.tm.archive.stories.StoryTextPostModel; 43import org.tm.archive.util.MediaUtil; 44import org.tm.archive.util.Projection; 45import org.tm.archive.util.Util; 46import org.tm.archive.util.views.Stub; 47 48import java.io.IOException; 49import java.util.List; 50 51public class QuoteView extends ConstraintLayout implements RecipientForeverObserver { 52 53 private static final String TAG = Log.tag(QuoteView.class); 54 55 public enum MessageType { 56 // These codes must match the values for the QuoteView_message_type XML attribute. 57 PREVIEW(0), 58 OUTGOING(1), 59 INCOMING(2), 60 STORY_REPLY_OUTGOING(3), 61 STORY_REPLY_INCOMING(4), 62 STORY_REPLY_PREVIEW(5); 63 64 private final int code; 65 66 MessageType(int code) { 67 this.code = code; 68 } 69 70 private static @NonNull MessageType fromCode(int code) { 71 for (MessageType value : values()) { 72 if (value.code == code) { 73 return value; 74 } 75 } 76 77 throw new IllegalArgumentException("Unsupported code " + code); 78 } 79 } 80 81 private TextView authorView; 82 private EmojiTextView bodyView; 83 private View quoteBarView; 84 private ShapeableImageView thumbnailView; 85 private Stub<View> attachmentVideoOVerlayStub; 86 private Stub<TextView> attachmentNameViewStub; 87 private Stub<ImageView> dismissStub; 88 private EmojiImageView missingStoryReaction; 89 private EmojiImageView storyReactionEmoji; 90 91 private long id; 92 private LiveRecipient author; 93 private CharSequence body; 94 private TextView mediaDescriptionText; 95 private Stub<TextView> missingLinkTextStub; 96 private SlideDeck attachments; 97 private MessageType messageType; 98 private int largeCornerRadius; 99 private int smallCornerRadius; 100 private CornerMask cornerMask; 101 private QuoteModel.Type quoteType; 102 private boolean isWallpaperEnabled; 103 104 private int thumbHeight; 105 private int thumbWidth; 106 107 public QuoteView(Context context) { 108 super(context); 109 initialize(null); 110 } 111 112 public QuoteView(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 initialize(attrs); 115 } 116 117 public QuoteView(Context context, AttributeSet attrs, int defStyleAttr) { 118 super(context, attrs, defStyleAttr); 119 initialize(attrs); 120 } 121 122 public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 123 super(context, attrs, defStyleAttr, defStyleRes); 124 initialize(attrs); 125 } 126 127 private void initialize(@Nullable AttributeSet attrs) { 128 inflate(getContext(), R.layout.v2_quote_view, this); 129 130 this.authorView = findViewById(R.id.quote_author); 131 this.bodyView = findViewById(R.id.quote_text); 132 this.quoteBarView = findViewById(R.id.quote_bar); 133 this.thumbnailView = findViewById(R.id.quote_thumbnail); 134 this.attachmentVideoOVerlayStub = new Stub<>(findViewById(R.id.quote_video_overlay_stub)); 135 this.attachmentNameViewStub = new Stub<>(findViewById(R.id.quote_attachment_name_stub)); 136 this.dismissStub = new Stub<>(findViewById(R.id.quote_dismiss_stub)); 137 this.mediaDescriptionText = findViewById(R.id.media_type); 138 this.missingLinkTextStub = new Stub<>(findViewById(R.id.quote_missing_text_stub)); 139 this.missingStoryReaction = findViewById(R.id.quote_missing_story_reaction_emoji); 140 this.storyReactionEmoji = findViewById(R.id.quote_story_reaction_emoji); 141 this.largeCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_large); 142 this.smallCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom); 143 144 cornerMask = new CornerMask(this); 145 146 if (attrs != null) { 147 TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0); 148 149 messageType = MessageType.fromCode(typedArray.getInt(R.styleable.QuoteView_message_type, 0)); 150 typedArray.recycle(); 151 152 dismissStub.setVisibility(messageType == MessageType.PREVIEW ? VISIBLE : GONE); 153 } 154 155 setMessageType(messageType); 156 } 157 158 @Override 159 protected void dispatchDraw(Canvas canvas) { 160 super.dispatchDraw(canvas); 161 cornerMask.mask(canvas); 162 } 163 164 @Override 165 protected void onDetachedFromWindow() { 166 super.onDetachedFromWindow(); 167 if (author != null) author.removeForeverObserver(this); 168 } 169 170 public void setMessageType(@NonNull MessageType messageType) { 171 this.messageType = messageType; 172 173 cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius); 174 thumbWidth = thumbHeight = getResources().getDimensionPixelSize(R.dimen.quote_thumb_size); 175 176 if (messageType == MessageType.PREVIEW) { 177 int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview); 178 cornerMask.setTopLeftRadius(radius); 179 cornerMask.setTopRightRadius(radius); 180 } else if (isStoryReply()) { 181 thumbWidth = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_width); 182 thumbHeight = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_height); 183 } 184 185 ViewGroup.LayoutParams params = thumbnailView.getLayoutParams(); 186 params.height = thumbHeight; 187 params.width = thumbWidth; 188 189 thumbnailView.setLayoutParams(params); 190 dismissStub.setVisibility(messageType == MessageType.PREVIEW ? View.VISIBLE : View.GONE); 191 if (dismissStub.resolved()) { 192 dismissStub.get().setOnClickListener(view -> setVisibility(GONE)); 193 } 194 } 195 196 public void setQuote(RequestManager requestManager, 197 long id, 198 @NonNull Recipient author, 199 @Nullable CharSequence body, 200 boolean originalMissing, 201 @NonNull SlideDeck attachments, 202 @Nullable String storyReaction, 203 @NonNull QuoteModel.Type quoteType) 204 { 205 if (this.author != null) this.author.removeForeverObserver(this); 206 207 this.id = id; 208 this.author = author.live(); 209 this.body = body; 210 this.attachments = attachments; 211 this.quoteType = quoteType; 212 213 this.author.observeForever(this); 214 setQuoteAuthor(author); 215 setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction); 216 setQuoteAttachment(requestManager, body, attachments, originalMissing); 217 setQuoteMissingFooter(originalMissing); 218 applyColorTheme(); 219 } 220 221 private @Nullable CharSequence resolveBody(@Nullable CharSequence body, @NonNull QuoteModel.Type quoteType) { 222 return quoteType == QuoteModel.Type.GIFT_BADGE ? getContext().getString(R.string.QuoteView__donation_for_a_friend) : body; 223 } 224 225 public void setTopCornerSizes(boolean topLeftLarge, boolean topRightLarge) { 226 cornerMask.setTopLeftRadius(topLeftLarge ? largeCornerRadius : smallCornerRadius); 227 cornerMask.setTopRightRadius(topRightLarge ? largeCornerRadius : smallCornerRadius); 228 } 229 230 public void dismiss() { 231 if (this.author != null) this.author.removeForeverObserver(this); 232 233 this.id = 0; 234 this.author = null; 235 this.body = null; 236 237 setVisibility(GONE); 238 } 239 240 public void setWallpaperEnabled(boolean isWallpaperEnabled) { 241 this.isWallpaperEnabled = isWallpaperEnabled; 242 applyColorTheme(); 243 } 244 245 @Override 246 public void onRecipientChanged(@NonNull Recipient recipient) { 247 setQuoteAuthor(recipient); 248 } 249 public @NonNull Projection.Corners getCorners() { 250 return new Projection.Corners(cornerMask.getRadii()); 251 } 252 253 private void setQuoteAuthor(@NonNull Recipient author) { 254 if (isStoryReply()) { 255 authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_your_story) 256 : getContext().getString(R.string.QuoteView_s_story, author.getDisplayName(getContext()))); 257 } else { 258 authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you) 259 : author.getDisplayName(getContext())); 260 } 261 } 262 263 private boolean isStoryReply() { 264 return messageType == MessageType.STORY_REPLY_OUTGOING || 265 messageType == MessageType.STORY_REPLY_INCOMING || 266 messageType == MessageType.STORY_REPLY_PREVIEW; 267 } 268 269 private void setQuoteText(@Nullable CharSequence body, 270 @NonNull SlideDeck attachments, 271 boolean originalMissing, 272 @Nullable String storyReaction) 273 { 274 if (originalMissing && isStoryReply()) { 275 bodyView.setVisibility(GONE); 276 storyReactionEmoji.setVisibility(View.GONE); 277 mediaDescriptionText.setVisibility(VISIBLE); 278 279 mediaDescriptionText.setText(R.string.QuoteView_no_longer_available); 280 if (storyReaction != null) { 281 missingStoryReaction.setVisibility(View.VISIBLE); 282 missingStoryReaction.setImageEmoji(storyReaction); 283 } else { 284 missingStoryReaction.setVisibility(View.GONE); 285 } 286 return; 287 } 288 289 if (storyReaction != null) { 290 storyReactionEmoji.setImageEmoji(storyReaction); 291 storyReactionEmoji.setVisibility(View.VISIBLE); 292 missingStoryReaction.setVisibility(View.INVISIBLE); 293 } else { 294 storyReactionEmoji.setVisibility(View.GONE); 295 missingStoryReaction.setVisibility(View.GONE); 296 } 297 298 StoryTextPostModel textPostModel = isStoryReply() ? getStoryTextPost(body) : null; 299 if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) { 300 if (textPostModel != null) { 301 try { 302 bodyView.setText(textPostModel.getText()); 303 } catch (Exception e) { 304 Log.w(TAG, "Could not parse body of text post.", e); 305 bodyView.setText(""); 306 } 307 } else { 308 bodyView.setText(body == null ? "" : body); 309 } 310 311 bodyView.setVisibility(VISIBLE); 312 mediaDescriptionText.setVisibility(GONE); 313 return; 314 } 315 316 bodyView.setVisibility(GONE); 317 mediaDescriptionText.setVisibility(VISIBLE); 318 319 Slide audioSlide = attachments.getSlides().stream().filter(Slide::hasAudio).findFirst().orElse(null); 320 Slide documentSlide = attachments.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null); 321 Slide imageSlide = attachments.getSlides().stream().filter(Slide::hasImage).findFirst().orElse(null); 322 Slide videoSlide = attachments.getSlides().stream().filter(Slide::hasVideo).findFirst().orElse(null); 323 Slide stickerSlide = attachments.getSlides().stream().filter(Slide::hasSticker).findFirst().orElse(null); 324 Slide viewOnceSlide = attachments.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null); 325 326 // Given that most types have images, we specifically check images last 327 if (viewOnceSlide != null) { 328 mediaDescriptionText.setText(R.string.QuoteView_view_once_media); 329 } else if (audioSlide != null) { 330 mediaDescriptionText.setText(R.string.QuoteView_audio); 331 } else if (documentSlide != null) { 332 mediaDescriptionText.setVisibility(GONE); 333 } else if (videoSlide != null) { 334 if (videoSlide.isVideoGif()) { 335 mediaDescriptionText.setText(R.string.QuoteView_gif); 336 } else { 337 mediaDescriptionText.setText(R.string.QuoteView_video); 338 } 339 } else if (stickerSlide != null) { 340 mediaDescriptionText.setText(R.string.QuoteView_sticker); 341 } else if (imageSlide != null) { 342 if (MediaUtil.isGif(imageSlide.getContentType())) { 343 mediaDescriptionText.setText(R.string.QuoteView_gif); 344 } else { 345 mediaDescriptionText.setText(R.string.QuoteView_photo); 346 } 347 } 348 } 349 350 private void setQuoteAttachment(@NonNull RequestManager requestManager, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) { 351 boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING; 352 boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW; 353 354 if (isStoryReply() && originalMissing) { 355 thumbnailView.setVisibility(GONE); 356 attachmentVideoOVerlayStub.setVisibility(GONE); 357 attachmentNameViewStub.setVisibility(GONE); 358 return; 359 } 360 361 // TODO [alex] -- do we need this? mainView.setMinimumHeight(isStoryReply() && originalMissing ? 0 : thumbHeight); 362 thumbnailView.setPadding(0, 0, 0, 0); 363 364 StoryTextPostModel model = isStoryReply() ? getStoryTextPost(body) : null; 365 if (model != null) { 366 attachmentVideoOVerlayStub.setVisibility(GONE); 367 attachmentNameViewStub.setVisibility(GONE); 368 thumbnailView.setVisibility(VISIBLE); 369 requestManager.load(model) 370 .centerCrop() 371 .override(thumbWidth, thumbHeight) 372 .diskCacheStrategy(DiskCacheStrategy.RESOURCE) 373 .into(thumbnailView); 374 return; 375 } 376 377 if (quoteType == QuoteModel.Type.GIFT_BADGE) { 378 if (outgoing && !preview) { 379 int oneDp = (int) DimensionUnit.DP.toPixels(1); 380 thumbnailView.setPadding(oneDp, oneDp, oneDp, oneDp); 381 thumbnailView.setShapeAppearanceModel(buildShapeAppearanceForLayoutDirection()); 382 } 383 384 attachmentVideoOVerlayStub.setVisibility(GONE); 385 attachmentNameViewStub.setVisibility(GONE); 386 thumbnailView.setVisibility(VISIBLE); 387 requestManager.load(R.drawable.ic_gift_thumbnail) 388 .centerCrop() 389 .override(thumbWidth, thumbHeight) 390 .diskCacheStrategy(DiskCacheStrategy.RESOURCE) 391 .into(thumbnailView); 392 return; 393 } 394 395 Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null); 396 Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null); 397 Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null); 398 399 attachmentVideoOVerlayStub.setVisibility(GONE); 400 401 if (viewOnceSlide != null) { 402 thumbnailView.setVisibility(GONE); 403 attachmentNameViewStub.setVisibility(GONE); 404 } else if (imageVideoSlide != null && imageVideoSlide.getUri() != null) { 405 thumbnailView.setVisibility(VISIBLE); 406 attachmentNameViewStub.setVisibility(GONE); 407 408 if (dismissStub.resolved()) { 409 dismissStub.get().setBackgroundResource(R.drawable.dismiss_background); 410 } 411 if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) { 412 attachmentVideoOVerlayStub.setVisibility(VISIBLE); 413 } 414 requestManager.load(new DecryptableUri(imageVideoSlide.getUri())) 415 .centerCrop() 416 .override(thumbWidth, thumbHeight) 417 .diskCacheStrategy(DiskCacheStrategy.RESOURCE) 418 .into(thumbnailView); 419 } else if (documentSlide != null){ 420 thumbnailView.setVisibility(GONE); 421 attachmentNameViewStub.setVisibility(VISIBLE); 422 attachmentNameViewStub.get().setText(documentSlide.getFileName().orElse("")); 423 } else { 424 thumbnailView.setVisibility(GONE); 425 attachmentNameViewStub.setVisibility(GONE); 426 427 if (dismissStub.resolved()) { 428 dismissStub.get().setBackground(null); 429 } 430 } 431 } 432 433 private void setQuoteMissingFooter(boolean missing) { 434 missingLinkTextStub.setVisibility(missing && !isStoryReply() ? VISIBLE : GONE); 435 } 436 437 private @Nullable StoryTextPostModel getStoryTextPost(@Nullable CharSequence body) { 438 if (Util.isEmpty(body)) { 439 return null; 440 } 441 442 try { 443 return StoryTextPostModel.parseFrom(body.toString(), id, author.getId(), MessageStyler.getStyling(body)); 444 } catch (IOException ioException) { 445 return null; 446 } 447 } 448 449 public void setTextSize(int unit, float size) { 450 bodyView.setTextSize(unit, size); 451 } 452 453 public long getQuoteId() { 454 return id; 455 } 456 457 public Recipient getAuthor() { 458 return author.get(); 459 } 460 461 public CharSequence getBody() { 462 return body; 463 } 464 465 public List<Attachment> getAttachments() { 466 return attachments.asAttachments(); 467 } 468 469 public @NonNull QuoteModel.Type getQuoteType() { 470 return quoteType; 471 } 472 473 public @NonNull List<Mention> getMentions() { 474 return MentionAnnotation.getMentionsFromAnnotations(body); 475 } 476 477 public @Nullable BodyRangeList getBodyRanges() { 478 return MessageStyler.getStyling(body); 479 } 480 481 private @NonNull ShapeAppearanceModel buildShapeAppearanceForLayoutDirection() { 482 int fourDp = (int) DimensionUnit.DP.toPixels(4); 483 if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) { 484 return ShapeAppearanceModel.builder() 485 .setTopRightCorner(CornerFamily.ROUNDED, fourDp) 486 .setBottomRightCorner(CornerFamily.ROUNDED, fourDp) 487 .build(); 488 } else { 489 return ShapeAppearanceModel.builder() 490 .setTopLeftCorner(CornerFamily.ROUNDED, fourDp) 491 .setBottomLeftCorner(CornerFamily.ROUNDED, fourDp) 492 .build(); 493 } 494 } 495 496 private void applyColorTheme() { 497 boolean isOutgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING; 498 boolean isPreview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW; 499 500 QuoteViewColorTheme quoteViewColorTheme = QuoteViewColorTheme.resolveTheme(isOutgoing, isPreview, isWallpaperEnabled); 501 502 quoteBarView.setBackgroundColor(quoteViewColorTheme.getBarColor(getContext())); 503 setBackgroundColor(quoteViewColorTheme.getBackgroundColor(getContext())); 504 authorView.setTextColor(quoteViewColorTheme.getForegroundColor(getContext())); 505 bodyView.setTextColor(quoteViewColorTheme.getForegroundColor(getContext())); 506 507 if (attachmentNameViewStub.resolved()) { 508 attachmentNameViewStub.get().setTextColor(quoteViewColorTheme.getForegroundColor(getContext())); 509 } 510 mediaDescriptionText.setTextColor(quoteViewColorTheme.getForegroundColor(getContext())); 511 512 if (missingLinkTextStub.resolved()) { 513 missingLinkTextStub.get().setTextColor(quoteViewColorTheme.getForegroundColor(getContext())); 514 missingLinkTextStub.get().setBackgroundColor(quoteViewColorTheme.getBackgroundColor(getContext())); 515 } 516 } 517}