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