That fuck shit the fascists are using
at master 683 lines 23 kB view raw
1/* 2 * Copyright (C) 2015 Open Whisper Systems 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17package org.tm.archive.mediaoverview; 18 19import android.content.Context; 20import android.net.Uri; 21import android.os.Handler; 22import android.os.Looper; 23import android.view.LayoutInflater; 24import android.view.View; 25import android.view.ViewGroup; 26import android.widget.TextView; 27 28import androidx.annotation.NonNull; 29import androidx.annotation.Nullable; 30import androidx.lifecycle.Observer; 31import androidx.recyclerview.widget.RecyclerView; 32 33import com.annimon.stream.Collectors; 34import com.annimon.stream.Stream; 35import com.bumptech.glide.RequestManager; 36import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter; 37 38import org.signal.libsignal.protocol.util.Pair; 39import org.tm.archive.R; 40import org.tm.archive.attachments.AttachmentId; 41import org.tm.archive.components.AudioView; 42import org.tm.archive.components.ThumbnailView; 43import org.tm.archive.components.voice.VoiceNotePlaybackState; 44import org.tm.archive.database.MediaTable; 45import org.tm.archive.database.MediaTable.MediaRecord; 46import org.tm.archive.database.loaders.GroupedThreadMediaLoader.GroupedThreadMedia; 47import org.tm.archive.mediapreview.MediaPreviewCache; 48import org.tm.archive.mms.AudioSlide; 49import org.tm.archive.mms.Slide; 50import org.tm.archive.recipients.LiveRecipient; 51import org.tm.archive.recipients.Recipient; 52import org.tm.archive.util.DateUtils; 53import org.tm.archive.util.MediaUtil; 54import org.tm.archive.util.Util; 55import org.tm.archive.util.livedata.LiveDataPair; 56 57import java.util.Collection; 58import java.util.HashMap; 59import java.util.HashSet; 60import java.util.List; 61import java.util.Locale; 62import java.util.Map; 63import java.util.Objects; 64import java.util.Optional; 65import java.util.concurrent.TimeUnit; 66 67final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { 68 69 private static final long SELECTION_ANIMATION_DURATION = TimeUnit.MILLISECONDS.toMillis(150); 70 71 private final Context context; 72 private final boolean showThread; 73 private final RequestManager requestManager; 74 private final ItemClickListener itemClickListener; 75 private final Map<AttachmentId, MediaRecord> selected = new HashMap<>(); 76 private final AudioItemListener audioItemListener; 77 78 private GroupedThreadMedia media; 79 private boolean showFileSizes; 80 private boolean detailView; 81 82 private static final int AUDIO_DETAIL = 1; 83 public static final int GALLERY = 2; 84 private static final int GALLERY_DETAIL = 3; 85 private static final int DOCUMENT_DETAIL = 4; 86 87 private static final int PAYLOAD_SELECTED = 1; 88 89 void detach(RecyclerView.ViewHolder holder) { 90 if (holder instanceof SelectableViewHolder) { 91 ((SelectableViewHolder) holder).onDetached(); 92 } 93 } 94 95 private static class HeaderHolder extends HeaderViewHolder { 96 TextView textView; 97 98 HeaderHolder(View itemView) { 99 super(itemView); 100 textView = itemView.findViewById(R.id.text); 101 } 102 } 103 104 MediaGalleryAllAdapter(@NonNull Context context, 105 @NonNull RequestManager requestManager, 106 GroupedThreadMedia media, 107 ItemClickListener clickListener, 108 @NonNull AudioItemListener audioItemListener, 109 boolean showFileSizes, 110 boolean showThread) 111 { 112 this.context = context; 113 this.requestManager = requestManager; 114 this.media = media; 115 this.itemClickListener = clickListener; 116 this.audioItemListener = audioItemListener; 117 this.showFileSizes = showFileSizes; 118 this.showThread = showThread; 119 } 120 121 public void setMedia(GroupedThreadMedia media) { 122 this.media = media; 123 } 124 125 @Override 126 public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) { 127 return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_item_header, parent, false)); 128 } 129 130 @Override 131 public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) { 132 switch (itemType) { 133 case GALLERY: 134 return new GalleryViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_gallery_item, parent, false)); 135 case GALLERY_DETAIL: 136 return new GalleryDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_media, parent, false)); 137 case AUDIO_DETAIL: 138 return new AudioDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_audio, parent, false)); 139 default: 140 return new DocumentDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_document, parent, false)); 141 } 142 } 143 144 @Override 145 public int getSectionItemViewType(int section, int offset) { 146 MediaTable.MediaRecord mediaRecord = media.get(section, offset); 147 Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment()); 148 149 if (slide.hasAudio()) return AUDIO_DETAIL; 150 if (slide.hasImage() || slide.hasVideo()) return detailView ? GALLERY_DETAIL : GALLERY; 151 if (slide.hasDocument()) return DOCUMENT_DETAIL; 152 153 return 0; 154 } 155 156 @Override 157 public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section) { 158 ((HeaderHolder) viewHolder).textView.setText(media.getName(section)); 159 } 160 161 @Override 162 public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) { 163 if (holder instanceof SelectableViewHolder && payloads.contains(PAYLOAD_SELECTED)) { 164 SelectableViewHolder selectableViewHolder = (SelectableViewHolder) holder; 165 selectableViewHolder.animateSelectedView(); 166 } else { 167 super.onBindViewHolder(holder, position, payloads); 168 } 169 } 170 171 @Override 172 public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) { 173 MediaTable.MediaRecord mediaRecord = media.get(section, offset); 174 Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment()); 175 176 ((SelectableViewHolder) viewHolder).bind(context, mediaRecord, slide); 177 } 178 179 @Override 180 public void onViewDetachedFromWindow(@NonNull ViewHolder holder) { 181 super.onViewDetachedFromWindow(holder); 182 if (holder instanceof SelectableViewHolder) { 183 ((SelectableViewHolder) holder).onDetached(); 184 } 185 } 186 187 @Override 188 public void onViewAttachedToWindow(@NonNull ViewHolder holder) { 189 super.onViewAttachedToWindow(holder); 190 if (holder instanceof SelectableViewHolder) { 191 ((SelectableViewHolder) holder).onAttached(); 192 } 193 } 194 195 @Override 196 public int getSectionCount() { 197 return media.getSectionCount(); 198 } 199 200 @Override 201 public int getSectionItemCount(int section) { 202 return media.getSectionItemCount(section); 203 } 204 205 public void toggleSelection(@NonNull MediaRecord mediaRecord) { 206 AttachmentId attachmentId = mediaRecord.getAttachment().attachmentId; 207 MediaTable.MediaRecord removed = selected.remove(attachmentId); 208 if (removed == null) { 209 selected.put(attachmentId, mediaRecord); 210 } 211 212 notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED); 213 } 214 215 public int getSelectedMediaCount() { 216 return selected.size(); 217 } 218 219 public long getSelectedMediaTotalFileSize() { 220 //noinspection ConstantConditions attacment cannot be null if selected 221 return Stream.of(selected.values()) 222 .collect(Collectors.summingLong(a -> a.getAttachment().size)); 223 } 224 225 @NonNull 226 public Collection<MediaRecord> getSelectedMedia() { 227 return new HashSet<>(selected.values()); 228 } 229 230 public void clearSelection() { 231 selected.clear(); 232 notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED); 233 } 234 235 void selectAllMedia() { 236 int sectionCount = media.getSectionCount(); 237 for (int section = 0; section < sectionCount; section++) { 238 int sectionItemCount = media.getSectionItemCount(section); 239 for (int item = 0; item < sectionItemCount; item++) { 240 MediaRecord mediaRecord = media.get(section, item); 241 selected.put(mediaRecord.getAttachment().attachmentId, mediaRecord); 242 } 243 } 244 this.notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED); 245 } 246 247 void setShowFileSizes(boolean showFileSizes) { 248 this.showFileSizes = showFileSizes; 249 } 250 251 void setDetailView(boolean detailView) { 252 this.detailView = detailView; 253 } 254 255 class SelectableViewHolder extends ItemViewHolder { 256 257 protected final View selectedIndicator; 258 259 private MediaTable.MediaRecord mediaRecord; 260 private boolean bound; 261 262 SelectableViewHolder(@NonNull View itemView) { 263 super(itemView); 264 this.selectedIndicator = itemView.findViewById(R.id.selected_indicator); 265 } 266 267 public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) { 268 if (bound) { 269 unbind(); 270 } 271 this.mediaRecord = mediaRecord; 272 updateSelectedView(); 273 bound = true; 274 } 275 276 void rebind() { 277 bound = true; 278 } 279 280 void unbind() { 281 bound = false; 282 } 283 284 protected boolean isSelected() { 285 return selected.containsKey(mediaRecord.getAttachment().attachmentId); 286 } 287 288 protected void updateSelectedView() { 289 if (selectedIndicator != null) { 290 selectedIndicator.animate().cancel(); 291 selectedIndicator.setAlpha(isSelected() ? 1f : 0f); 292 } 293 } 294 295 protected void animateSelectedView() { 296 if (selectedIndicator != null) { 297 selectedIndicator.animate() 298 .alpha(isSelected() ? 1f : 0f) 299 .setDuration(SELECTION_ANIMATION_DURATION); 300 } 301 } 302 303 boolean onLongClick() { 304 itemClickListener.onMediaLongClicked(mediaRecord); 305 return true; 306 } 307 308 void onDetached() { 309 if (bound) { 310 unbind(); 311 } 312 } 313 314 void onAttached() { 315 if (!bound) { 316 rebind(); 317 } 318 } 319 } 320 321 private class GalleryViewHolder extends SelectableViewHolder { 322 323 private static final float SCALE_SELECTED = 0.83f; 324 private static final float SCALE_NORMAL = 1f; 325 326 private final ThumbnailView thumbnailView; 327 private final TextView imageFileSize; 328 329 private Slide slide; 330 331 GalleryViewHolder(@NonNull View itemView) { 332 super(itemView); 333 this.thumbnailView = itemView.findViewById(R.id.image); 334 this.imageFileSize = itemView.findViewById(R.id.image_file_size); 335 } 336 337 @Override 338 public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) { 339 super.bind(context, mediaRecord, slide); 340 this.slide = slide; 341 if (showFileSizes | detailView) { 342 imageFileSize.setText(Util.getPrettyFileSize(slide.getFileSize())); 343 imageFileSize.setVisibility(View.VISIBLE); 344 } else { 345 imageFileSize.setVisibility(View.GONE); 346 } 347 348 thumbnailView.setImageResource(requestManager, slide, false, false); 349 thumbnailView.setOnClickListener(view -> { 350 MediaPreviewCache.INSTANCE.setDrawable(thumbnailView.getImageDrawable()); 351 itemClickListener.onMediaClicked(thumbnailView, mediaRecord); 352 }); 353 thumbnailView.setOnLongClickListener(view -> onLongClick()); 354 } 355 356 @Override 357 protected void updateSelectedView() { 358 super.updateSelectedView(); 359 360 thumbnailView.animate().cancel(); 361 362 float scale = isSelected() ? SCALE_SELECTED : SCALE_NORMAL; 363 thumbnailView.setScaleX(scale); 364 thumbnailView.setScaleY(scale); 365 } 366 367 @Override 368 void rebind() { 369 thumbnailView.setImageResource(requestManager, slide, false, false); 370 super.rebind(); 371 } 372 373 @Override 374 void unbind() { 375 thumbnailView.clear(requestManager); 376 super.unbind(); 377 } 378 379 @Override 380 public void animateSelectedView() { 381 super.animateSelectedView(); 382 383 float scale = isSelected() ? SCALE_SELECTED : SCALE_NORMAL; 384 thumbnailView.animate() 385 .scaleX(scale) 386 .scaleY(scale) 387 .setDuration(SELECTION_ANIMATION_DURATION); 388 } 389 } 390 391 private abstract class DetailViewHolder extends SelectableViewHolder implements Observer<Pair<Recipient, Recipient>> { 392 393 protected final View itemView; 394 private final TextView line1; 395 private final TextView line2; 396 private LiveDataPair<Recipient, Recipient> liveDataPair; 397 private Optional<String> fileName; 398 private String fileTypeDescription; 399 private Handler handler; 400 private Runnable selectForMarque; 401 402 DetailViewHolder(@NonNull View itemView) { 403 super(itemView); 404 this.line1 = itemView.findViewById(R.id.line1); 405 this.line2 = itemView.findViewById(R.id.line2); 406 this.itemView = itemView; 407 } 408 409 @Override 410 public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) { 411 super.bind(context, mediaRecord, slide); 412 413 fileName = slide.getFileName(); 414 fileTypeDescription = getFileTypeDescription(context, slide); 415 416 line1.setText(fileName.orElse(fileTypeDescription)); 417 line2.setText(getLine2(context, mediaRecord, slide)); 418 itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(getTransitionAnchor(), mediaRecord)); 419 itemView.setOnLongClickListener(view -> onLongClick()); 420 selectForMarque = () -> line1.setSelected(true); 421 handler = new Handler(Looper.getMainLooper()); 422 handler.postDelayed(selectForMarque, 2500); 423 424 LiveRecipient from = mediaRecord.isOutgoing() ? Recipient.self().live() : Recipient.live(mediaRecord.getRecipientId()); 425 LiveRecipient to = Recipient.live(mediaRecord.getThreadRecipientId()); 426 427 liveDataPair = new LiveDataPair<>(from.getLiveData(), to.getLiveData(), Recipient.UNKNOWN, Recipient.UNKNOWN); 428 liveDataPair.observeForever(this); 429 } 430 431 @Override 432 void rebind() { 433 liveDataPair.observeForever(this); 434 handler.postDelayed(selectForMarque, 2500); 435 super.rebind(); 436 } 437 438 @Override 439 void unbind() { 440 liveDataPair.removeObserver(this); 441 handler.removeCallbacks(selectForMarque); 442 line1.setSelected(false); 443 super.unbind(); 444 } 445 446 private String getLine2(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) { 447 return context.getString(R.string.MediaOverviewActivity_detail_line_3_part, 448 Util.getPrettyFileSize(slide.getFileSize()), 449 getFileTypeDescription(context, slide), 450 DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), mediaRecord.getDate())); 451 } 452 453 protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) { 454 return context.getString(R.string.MediaOverviewActivity_file); 455 } 456 457 @Override 458 public void onChanged(Pair<Recipient, Recipient> fromToPair) { 459 line1.setText(describe(fromToPair.first(), fromToPair.second())); 460 } 461 462 protected @Nullable String getMediaTitle() { 463 return fileName.orElse(null); 464 } 465 466 protected @NonNull View getTransitionAnchor() { 467 return itemView; 468 } 469 470 private @NonNull String describe(@NonNull Recipient from, @NonNull Recipient thread) { 471 if (from == Recipient.UNKNOWN && thread == Recipient.UNKNOWN) { 472 return fileName.orElse(fileTypeDescription); 473 } 474 475 String sentFromToString = getSentFromToString(from, thread); 476 String mediaTitle = getMediaTitle(); 477 478 if (mediaTitle != null) { 479 return context.getString(R.string.MediaOverviewActivity_detail_line_2_part, 480 mediaTitle, 481 sentFromToString); 482 } else { 483 return sentFromToString; 484 } 485 } 486 487 private String getSentFromToString(@NonNull Recipient from, @NonNull Recipient thread) { 488 if (from.isSelf() && from == thread) { 489 return context.getString(R.string.note_to_self); 490 } 491 492 if (showThread && (from.isSelf() || thread.isGroup())) { 493 if (from.isSelf()) { 494 return context.getString(R.string.MediaOverviewActivity_sent_by_you_to_s, thread.getDisplayName(context)); 495 } else { 496 return context.getString(R.string.MediaOverviewActivity_sent_by_s_to_s, from.getDisplayName(context), thread.getDisplayName(context)); 497 } 498 } else { 499 if (from.isSelf()) { 500 return context.getString(R.string.MediaOverviewActivity_sent_by_you); 501 } else { 502 return context.getString(R.string.MediaOverviewActivity_sent_by_s, from.getDisplayName(context)); 503 } 504 } 505 } 506 } 507 508 private class DocumentDetailViewHolder extends DetailViewHolder { 509 510 private final TextView documentType; 511 512 DocumentDetailViewHolder(@NonNull View itemView) { 513 super(itemView); 514 this.documentType = itemView.findViewById(R.id.document_extension); 515 } 516 517 @Override 518 public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) { 519 super.bind(context, mediaRecord, slide); 520 521 documentType.setText(slide.getFileType(context).orElse("").toLowerCase()); 522 } 523 } 524 525 private class AudioDetailViewHolder extends DetailViewHolder { 526 527 private final AudioView audioView; 528 529 private boolean isVoiceNote; 530 531 AudioDetailViewHolder(@NonNull View itemView) { 532 super(itemView); 533 this.audioView = itemView.findViewById(R.id.audio); 534 } 535 536 @Override 537 public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) { 538 if (!slide.hasAudio()) { 539 throw new AssertionError(); 540 } 541 542 isVoiceNote = slide.asAttachment().voiceNote; 543 544 super.bind(context, mediaRecord, slide); 545 546 long mmsId = Objects.requireNonNull(mediaRecord.getAttachment()).mmsId; 547 548 audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver()); 549 audioView.setAudio((AudioSlide) slide, new AudioViewCallbacksAdapter(audioItemListener, mmsId), true, true); 550 audioItemListener.registerPlaybackStateObserver(audioView.getPlaybackStateObserver()); 551 552 audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(audioView, mediaRecord)); 553 itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(audioView, mediaRecord)); 554 } 555 556 @Override 557 protected @NonNull String getMediaTitle() { 558 return context.getString(R.string.ThreadRecord_voice_message); 559 } 560 561 @Override 562 void rebind() { 563 super.rebind(); 564 audioItemListener.registerPlaybackStateObserver(audioView.getPlaybackStateObserver()); 565 } 566 567 @Override 568 void unbind() { 569 super.unbind(); 570 audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver()); 571 } 572 573 @Override 574 protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) { 575 return context.getString(R.string.MediaOverviewActivity_audio); 576 } 577 } 578 579 private class GalleryDetailViewHolder extends DetailViewHolder { 580 581 private final ThumbnailView thumbnailView; 582 583 private Slide slide; 584 585 GalleryDetailViewHolder(@NonNull View itemView) { 586 super(itemView); 587 this.thumbnailView = itemView.findViewById(R.id.image); 588 } 589 590 @Override 591 public void bind(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord, @NonNull Slide slide) { 592 super.bind(context, mediaRecord, slide); 593 this.slide = slide; 594 thumbnailView.setImageResource(requestManager, slide, false, false); 595 thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(thumbnailView, mediaRecord)); 596 thumbnailView.setOnLongClickListener(view -> onLongClick()); 597 } 598 599 @Override 600 protected @NonNull View getTransitionAnchor() { 601 MediaPreviewCache.INSTANCE.setDrawable(null); 602 return thumbnailView; 603 } 604 605 @Override 606 protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) { 607 if (slide.hasVideo()) return context.getString(R.string.MediaOverviewActivity_video); 608 if (slide.hasImage()) return context.getString(R.string.MediaOverviewActivity_image); 609 return super.getFileTypeDescription(context, slide); 610 } 611 612 @Override 613 void rebind() { 614 thumbnailView.setImageResource(requestManager, slide, false, false); 615 super.rebind(); 616 } 617 618 @Override 619 void unbind() { 620 thumbnailView.clear(requestManager); 621 super.unbind(); 622 } 623 } 624 625 private static final class AudioViewCallbacksAdapter implements AudioView.Callbacks { 626 627 private final AudioItemListener audioItemListener; 628 private final long messageId; 629 630 private AudioViewCallbacksAdapter(@NonNull AudioItemListener audioItemListener, long messageId) { 631 this.audioItemListener = audioItemListener; 632 this.messageId = messageId; 633 } 634 635 @Override 636 public void onPlay(@NonNull Uri audioUri, double progress) { 637 audioItemListener.onPlay(audioUri, progress, messageId); 638 } 639 640 @Override 641 public void onPause(@NonNull Uri audioUri) { 642 audioItemListener.onPause(audioUri); 643 } 644 645 @Override 646 public void onSeekTo(@NonNull Uri audioUri, double progress) { 647 audioItemListener.onSeekTo(audioUri, progress); 648 } 649 650 @Override 651 public void onStopAndReset(@NonNull Uri audioUri) { 652 audioItemListener.onStopAndReset(audioUri); 653 } 654 655 @Override 656 public void onSpeedChanged(float speed, boolean isPlaying) { 657 } 658 659 @Override 660 public void onProgressUpdated(long durationMillis, long playheadMillis) { 661 } 662 } 663 664 interface ItemClickListener { 665 void onMediaClicked(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord); 666 667 void onMediaLongClicked(MediaTable.MediaRecord mediaRecord); 668 } 669 670 interface AudioItemListener { 671 void onPlay(@NonNull Uri audioUri, double progress, long messageId); 672 673 void onPause(@NonNull Uri audioUri); 674 675 void onSeekTo(@NonNull Uri audioUri, double progress); 676 677 void onStopAndReset(@NonNull Uri audioUri); 678 679 void registerPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer); 680 681 void unregisterPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer); 682 } 683}