That fuck shit the fascists are using
1/*
2 * Copyright 2023 Signal Messenger, LLC
3 * SPDX-License-Identifier: AGPL-3.0-only
4 */
5
6package org.tm.archive.components;
7
8import android.content.Context;
9import android.content.res.TypedArray;
10import android.graphics.Bitmap;
11import android.graphics.Canvas;
12import android.graphics.PorterDuff;
13import android.graphics.PorterDuffColorFilter;
14import android.graphics.drawable.Drawable;
15import android.net.Uri;
16import android.os.Build;
17import android.util.AttributeSet;
18import android.view.View;
19import android.view.ViewGroup;
20import android.widget.FrameLayout;
21import android.widget.ImageView;
22
23import androidx.annotation.NonNull;
24import androidx.annotation.Nullable;
25import androidx.annotation.Px;
26import androidx.annotation.UiThread;
27import androidx.appcompat.widget.AppCompatImageView;
28
29import com.bumptech.glide.RequestBuilder;
30import com.bumptech.glide.RequestManager;
31import com.bumptech.glide.load.engine.DiskCacheStrategy;
32import com.bumptech.glide.request.Request;
33import com.bumptech.glide.request.RequestListener;
34import com.bumptech.glide.request.RequestOptions;
35
36import org.signal.core.util.concurrent.ListenableFuture;
37import org.signal.core.util.concurrent.SettableFuture;
38import org.signal.core.util.logging.Log;
39import org.signal.glide.transforms.SignalDownsampleStrategy;
40import org.tm.archive.R;
41import org.tm.archive.blurhash.BlurHash;
42import org.tm.archive.components.transfercontrols.TransferControlView;
43import org.tm.archive.database.AttachmentTable;
44import org.tm.archive.mms.DecryptableStreamUriLoader.DecryptableUri;
45import org.tm.archive.mms.ImageSlide;
46import org.tm.archive.mms.Slide;
47import org.tm.archive.mms.SlideClickListener;
48import org.tm.archive.mms.SlidesClickedListener;
49import org.tm.archive.mms.VideoSlide;
50import org.tm.archive.stories.StoryTextPostModel;
51import org.tm.archive.util.MediaUtil;
52import org.tm.archive.util.Util;
53import org.tm.archive.util.views.Stub;
54
55import java.util.Arrays;
56import java.util.Collections;
57import java.util.List;
58import java.util.Locale;
59import java.util.Objects;
60import java.util.concurrent.ExecutionException;
61
62import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
63
64public class ThumbnailView extends FrameLayout {
65
66 private static final String TAG = Log.tag(ThumbnailView.class);
67 private static final int WIDTH = 0;
68 private static final int HEIGHT = 1;
69 private static final int MIN_WIDTH = 0;
70 private static final int MAX_WIDTH = 1;
71 private static final int MIN_HEIGHT = 2;
72 private static final int MAX_HEIGHT = 3;
73
74 private final ImageView image;
75 private final ImageView blurHash;
76 private final View playOverlay;
77 private final View captionIcon;
78 private final AppCompatImageView errorImage;
79
80 private OnClickListener parentClickListener;
81
82 private final int[] dimens = new int[2];
83 private final int[] bounds = new int[4];
84 private final int[] measureDimens = new int[2];
85
86 private final CornerMask cornerMask;
87
88 private final Stub<TransferControlView> transferControlViewStub;
89 private SlideClickListener thumbnailClickListener = null;
90 private SlidesClickedListener startTransferClickListener = null;
91 private SlidesClickedListener cancelTransferClickListener = null;
92 private SlideClickListener playVideoClickListener = null;
93 private Slide slide = null;
94
95
96 public ThumbnailView(Context context) {
97 this(context, null);
98 }
99
100 public ThumbnailView(Context context, AttributeSet attrs) {
101 this(context, attrs, 0);
102 }
103
104 public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) {
105 super(context, attrs, defStyle);
106
107 inflate(context, R.layout.thumbnail_view, this);
108
109 this.image = findViewById(R.id.thumbnail_image);
110 this.blurHash = findViewById(R.id.thumbnail_blurhash);
111 this.playOverlay = findViewById(R.id.play_overlay);
112 this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
113 this.errorImage = findViewById(R.id.thumbnail_error);
114 this.cornerMask = new CornerMask(this);
115 this.transferControlViewStub = new Stub<>(findViewById(R.id.transfer_controls_stub));
116
117 super.setOnClickListener(new ThumbnailClickDispatcher());
118
119 if (attrs != null) {
120 TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0);
121 bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0);
122 bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
123 bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
124 bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
125
126 float radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius));
127 cornerMask.setRadius((int) radius);
128
129 int transparentOverlayColor = typedArray.getColor(R.styleable.ThumbnailView_transparent_overlay_color, -1);
130 if (transparentOverlayColor > 0) {
131 image.setColorFilter(new PorterDuffColorFilter(transparentOverlayColor, PorterDuff.Mode.SRC_ATOP));
132 } else {
133 image.setColorFilter(null);
134 }
135
136 typedArray.recycle();
137 } else {
138 float radius = getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius);
139 cornerMask.setRadius((int) radius);
140 image.setColorFilter(null);
141 }
142 }
143
144 @Override
145 protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasureSpec) {
146 fillTargetDimensions(measureDimens, dimens, bounds);
147 if (measureDimens[WIDTH] == 0 && measureDimens[HEIGHT] == 0) {
148 super.onMeasure(originalWidthMeasureSpec, originalHeightMeasureSpec);
149 return;
150 }
151
152 int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight();
153 int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom();
154
155 super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
156 MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
157 }
158
159 @SuppressWarnings("SpellCheckingInspection")
160 @Override
161 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
162 super.onSizeChanged(w, h, oldw, oldh);
163
164 float playOverlayScale = 1;
165 float captionIconScale = 1;
166 int playOverlayWidth = playOverlay.getLayoutParams().width;
167
168 if (playOverlayWidth * 2 > getWidth()) {
169 playOverlayScale /= 2;
170 captionIconScale = 0;
171 }
172
173 playOverlay.setScaleX(playOverlayScale);
174 playOverlay.setScaleY(playOverlayScale);
175
176 captionIcon.setScaleX(captionIconScale);
177 captionIcon.setScaleY(captionIconScale);
178 }
179
180 @Override
181 protected void dispatchDraw(Canvas canvas) {
182 super.dispatchDraw(canvas);
183
184 cornerMask.mask(canvas);
185 }
186
187 public void setMinimumThumbnailWidth(@Px int width) {
188 bounds[MIN_WIDTH] = width;
189 invalidate();
190 }
191
192 public void setMaximumThumbnailHeight(@Px int height) {
193 bounds[MAX_HEIGHT] = height;
194 invalidate();
195 }
196
197 @SuppressWarnings("SuspiciousNameCombination")
198 private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
199 int dimensFilledCount = getNonZeroCount(dimens);
200 int boundsFilledCount = getNonZeroCount(bounds);
201 boolean dimensAreInvalid = dimensFilledCount > 0 && dimensFilledCount < dimens.length;
202
203 if (dimensAreInvalid) {
204 Log.w(TAG, String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %d x %d", dimens[WIDTH], dimens[HEIGHT]));
205 }
206
207 if (dimensAreInvalid || dimensFilledCount == 0 || boundsFilledCount == 0) {
208 targetDimens[WIDTH] = 0;
209 targetDimens[HEIGHT] = 0;
210 return;
211 }
212
213 double naturalWidth = dimens[WIDTH];
214 double naturalHeight = dimens[HEIGHT];
215
216 int minWidth = bounds[MIN_WIDTH];
217 int maxWidth = bounds[MAX_WIDTH];
218 int minHeight = bounds[MIN_HEIGHT];
219 int maxHeight = bounds[MAX_HEIGHT];
220
221 if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) {
222 throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]",
223 minWidth, maxWidth, minHeight, maxHeight));
224 }
225
226 double measuredWidth = naturalWidth;
227 double measuredHeight = naturalHeight;
228
229 boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth;
230 boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight;
231
232 if (!widthInBounds || !heightInBounds) {
233 double minWidthRatio = naturalWidth / minWidth;
234 double maxWidthRatio = naturalWidth / maxWidth;
235 double minHeightRatio = naturalHeight / minHeight;
236 double maxHeightRatio = naturalHeight / maxHeight;
237
238 if (maxWidthRatio > 1 || maxHeightRatio > 1) {
239 if (maxWidthRatio >= maxHeightRatio) {
240 measuredWidth /= maxWidthRatio;
241 measuredHeight /= maxWidthRatio;
242 } else {
243 measuredWidth /= maxHeightRatio;
244 measuredHeight /= maxHeightRatio;
245 }
246
247 measuredWidth = Math.max(measuredWidth, minWidth);
248 measuredHeight = Math.max(measuredHeight, minHeight);
249
250 } else if (minWidthRatio < 1 || minHeightRatio < 1) {
251 if (minWidthRatio <= minHeightRatio) {
252 measuredWidth /= minWidthRatio;
253 measuredHeight /= minWidthRatio;
254 } else {
255 measuredWidth /= minHeightRatio;
256 measuredHeight /= minHeightRatio;
257 }
258
259 measuredWidth = Math.min(measuredWidth, maxWidth);
260 measuredHeight = Math.min(measuredHeight, maxHeight);
261 }
262 }
263
264 targetDimens[WIDTH] = (int) measuredWidth;
265 targetDimens[HEIGHT] = (int) measuredHeight;
266 }
267
268 private int getNonZeroCount(int[] values) {
269 int count = 0;
270 for (int val : values) {
271 if (val > 0) {
272 count++;
273 }
274 }
275 return count;
276 }
277
278 @Override
279 public void setOnClickListener(OnClickListener l) {
280 parentClickListener = l;
281 }
282
283 @Override
284 public void setFocusable(boolean focusable) {
285 super.setFocusable(focusable);
286 transferControlViewStub.get().setFocusable(focusable);
287 }
288
289 @Override
290 public void setClickable(boolean clickable) {
291 super.setClickable(clickable);
292 transferControlViewStub.get().setClickable(clickable);
293 }
294
295 public @Nullable Drawable getImageDrawable() {
296 return image.getDrawable();
297 }
298
299 public void setBounds(int minWidth, int maxWidth, int minHeight, int maxHeight) {
300 final int oldMinWidth = bounds[MIN_WIDTH];
301 final int oldMaxWidth = bounds[MAX_WIDTH];
302 final int oldMinHeight = bounds[MIN_HEIGHT];
303 final int oldMaxHeight = bounds[MAX_HEIGHT];
304
305 bounds[MIN_WIDTH] = minWidth;
306 bounds[MAX_WIDTH] = maxWidth;
307 bounds[MIN_HEIGHT] = minHeight;
308 bounds[MAX_HEIGHT] = maxHeight;
309
310 if (oldMinWidth != minWidth || oldMaxWidth != maxWidth || oldMinHeight != minHeight || oldMaxHeight != maxHeight) {
311 Log.d(TAG, "setBounds: update {minW" + minWidth + ",maxW" + maxWidth + ",minH" + minHeight + ",maxH" + maxHeight + "}");
312 forceLayout();
313 }
314 }
315
316 public void setImageDrawable(@NonNull RequestManager requestManager, @Nullable Drawable drawable) {
317 requestManager.clear(image);
318 requestManager.clear(blurHash);
319
320 image.setImageDrawable(drawable);
321 blurHash.setImageDrawable(null);
322 }
323
324 @UiThread
325 public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Slide slide,
326 boolean showControls, boolean isPreview)
327 {
328 return setImageResource(requestManager, slide, showControls, isPreview, 0, 0);
329 }
330
331 @UiThread
332 public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Slide slide,
333 boolean showControls, boolean isPreview,
334 int naturalWidth, int naturalHeight)
335 {
336 if (slide.asAttachment().isPermanentlyFailed()) {
337 this.slide = slide;
338
339 transferControlViewStub.setVisibility(View.GONE);
340 playOverlay.setVisibility(View.GONE);
341
342 requestManager.clear(blurHash);
343 blurHash.setImageDrawable(null);
344
345 requestManager.clear(image);
346 image.setImageDrawable(null);
347
348 int errorImageResource;
349 if (slide instanceof ImageSlide) {
350 errorImageResource = R.drawable.ic_photo_slash_outline_24;
351 } else if (slide instanceof VideoSlide) {
352 errorImageResource = R.drawable.ic_video_slash_outline_24;
353 } else {
354 errorImageResource = R.drawable.ic_error_outline_24;
355 }
356 errorImage.setImageResource(errorImageResource);
357 errorImage.setVisibility(View.VISIBLE);
358
359 return new SettableFuture<>(true);
360 } else {
361 errorImage.setVisibility(View.GONE);
362 }
363
364 if (showControls) {
365 transferControlViewStub.get().setTransferClickListener(new DownloadClickDispatcher());
366 transferControlViewStub.get().setCancelClickListener(new CancelClickDispatcher());
367 if (MediaUtil.isInstantVideoSupported(slide)) {
368 transferControlViewStub.get().setInstantPlaybackClickListener(new InstantVideoClickDispatcher());
369 }
370 transferControlViewStub.get().setSlides(List.of(slide));
371 }
372 int transferState = TransferControlView.getTransferState(List.of(slide));
373 transferControlViewStub.get().setVisible(showControls && transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE);
374
375 if (slide.getUri() != null && slide.hasPlayOverlay() &&
376 (slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE || isPreview))
377 {
378 this.playOverlay.setVisibility(View.VISIBLE);
379 } else {
380 this.playOverlay.setVisibility(View.GONE);
381 }
382
383 if (hasSameContents(this.slide, slide)) {
384 Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getUri());
385 return new SettableFuture<>(false);
386 }
387
388 if (this.slide != null && this.slide.getFastPreflightId() != null &&
389 (!slide.hasVideo() || Util.equals(this.slide.getUri(), slide.getUri())) &&
390 Util.equals(this.slide.getFastPreflightId(), slide.getFastPreflightId()))
391 {
392 Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
393 this.slide = slide;
394 return new SettableFuture<>(false);
395 }
396
397 Log.i(TAG, "loading part with id " + slide.asAttachment().getUri()
398 + ", progress " + slide.getTransferState() + ", fast preflight id: " +
399 slide.asAttachment().fastPreflightId);
400
401 BlurHash previousBlurHash = this.slide != null ? this.slide.getPlaceholderBlur() : null;
402
403 this.slide = slide;
404
405 this.captionIcon.setVisibility(slide.getCaption().isPresent() ? VISIBLE : GONE);
406
407 dimens[WIDTH] = naturalWidth;
408 dimens[HEIGHT] = naturalHeight;
409
410 invalidate();
411
412 SettableFuture<Boolean> result = new SettableFuture<>();
413 boolean resultHandled = false;
414
415 if (slide.hasPlaceholder() && (previousBlurHash == null || !Objects.equals(slide.getPlaceholderBlur(), previousBlurHash))) {
416 buildPlaceholderRequestBuilder(requestManager, slide).into(new GlideBitmapListeningTarget(blurHash, result));
417 resultHandled = true;
418 } else if (!slide.hasPlaceholder()) {
419 requestManager.clear(blurHash);
420 blurHash.setImageDrawable(null);
421 }
422
423 if (slide.getUri() != null) {
424 if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
425 SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>();
426 thumbnailFuture.deferTo(result);
427 thumbnailFuture.addListener(new BlurHashClearListener(requestManager, blurHash));
428 }
429
430 buildThumbnailRequestBuilder(requestManager, slide).into(new GlideDrawableListeningTarget(image, result));
431
432 resultHandled = true;
433 } else {
434 requestManager.clear(image);
435 image.setImageDrawable(null);
436 }
437
438 if (!resultHandled) {
439 result.set(false);
440 }
441
442 return result;
443 }
444
445 public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri) {
446 return setImageResource(requestManager, uri, 0, 0);
447 }
448
449 public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri, int width, int height) {
450 return setImageResource(requestManager, uri, width, height, true, null);
451 }
452
453 public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri, int width, int height, boolean animate, @Nullable ThumbnailRequestListener listener) {
454 SettableFuture<Boolean> future = new SettableFuture<>();
455
456 transferControlViewStub.setVisibility(View.GONE);
457
458 RequestBuilder<Drawable> request = requestManager.load(new DecryptableUri(uri))
459 .diskCacheStrategy(DiskCacheStrategy.NONE)
460 .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
461 .listener(listener);
462
463 if (animate) {
464 request = request.transition(withCrossFade());
465 }
466
467 request = override(request, width, height);
468
469 GlideDrawableListeningTarget target = new GlideDrawableListeningTarget(image, future);
470 Request previousRequest = target.getRequest();
471 boolean previousRequestRunning = previousRequest != null && previousRequest.isRunning();
472 request.into(target);
473 if (listener != null) {
474 listener.onLoadScheduled();
475 if (previousRequestRunning) {
476 listener.onLoadCanceled();
477 }
478 }
479
480 blurHash.setImageDrawable(null);
481
482 return future;
483 }
484
485 public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull StoryTextPostModel model, int width, int height) {
486 SettableFuture<Boolean> future = new SettableFuture<>();
487
488 transferControlViewStub.setVisibility(View.GONE);
489
490 RequestBuilder<Drawable> request = requestManager.load(model)
491 .diskCacheStrategy(DiskCacheStrategy.NONE)
492 .placeholder(model.getPlaceholder())
493 .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
494 .transition(withCrossFade());
495
496 request = override(request, width, height);
497
498 request.into(new GlideDrawableListeningTarget(image, future));
499 blurHash.setImageDrawable(null);
500
501 return future;
502 }
503
504 private <T> RequestBuilder<T> override(@NonNull RequestBuilder<T> request, int width, int height) {
505 if (width > 0 && height > 0) {
506 Log.d(TAG, "override: apply w" + width + "xh" + height);
507 return request.override(width, height);
508 } else {
509 Log.d(TAG, "override: skip w" + width + "xh" + height);
510 return request;
511 }
512 }
513
514 public void setThumbnailClickListener(SlideClickListener listener) {
515 this.thumbnailClickListener = listener;
516 }
517
518 public void setStartTransferClickListener(SlidesClickedListener listener) {
519 this.startTransferClickListener = listener;
520 }
521
522 public void setCancelTransferClickListener(SlidesClickedListener listener) {
523 this.cancelTransferClickListener = listener;
524 }
525
526 public void setPlayVideoClickListener(SlideClickListener listener) {
527 this.playVideoClickListener = listener;
528 }
529
530 private static boolean hasSameContents(@Nullable Slide slide, @Nullable Slide other) {
531 if (Util.equals(slide, other)) {
532
533 if (slide != null && other != null) {
534 byte[] digestLeft = slide.asAttachment().remoteDigest;
535 byte[] digestRight = other.asAttachment().remoteDigest;
536
537 return Arrays.equals(digestLeft, digestRight);
538 }
539 }
540
541 return false;
542 }
543
544 private RequestBuilder<Drawable> buildThumbnailRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
545 RequestBuilder<Drawable> requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
546 .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
547 .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
548 .transition(withCrossFade()));
549
550 boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
551
552 if (slide.isInProgress() || doNotShowMissingThumbnailImage) {
553 return requestBuilder;
554 } else {
555 return requestBuilder.apply(RequestOptions.errorOf(R.drawable.missing_thumbnail));
556 }
557 }
558
559 public void clear(RequestManager requestManager) {
560 requestManager.clear(image);
561 image.setImageDrawable(null);
562
563 if (transferControlViewStub.resolved()) {
564 transferControlViewStub.get().clear();
565 }
566
567 requestManager.clear(blurHash);
568 blurHash.setImageDrawable(null);
569
570 slide = null;
571 }
572
573 public void showSecondaryText(boolean showSecondaryText) {
574 transferControlViewStub.get().setShowSecondaryText(showSecondaryText);
575 }
576
577 public void showProgressSpinner() {
578 transferControlViewStub.get().setVisible(true);
579 }
580
581 public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
582 image.setScaleType(scaleType);
583 }
584
585 protected void setRadius(int radius) {
586 cornerMask.setRadius(radius);
587 invalidate();
588 }
589
590 public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) {
591 cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
592 invalidate();
593 }
594
595
596 private RequestBuilder<Bitmap> buildPlaceholderRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
597 RequestBuilder<Bitmap> bitmap = requestManager.asBitmap();
598 BlurHash placeholderBlur = slide.getPlaceholderBlur();
599
600 if (placeholderBlur != null) {
601 bitmap = bitmap.load(placeholderBlur);
602 } else {
603 bitmap = bitmap.load(slide.getPlaceholderRes(getContext().getTheme()));
604 }
605
606 final RequestBuilder<Bitmap> resizedRequest = applySizing(bitmap.diskCacheStrategy(DiskCacheStrategy.NONE));
607 if (placeholderBlur != null) {
608 return resizedRequest.centerCrop();
609 } else {
610 return resizedRequest;
611 }
612 }
613
614 private <TranscodeType> RequestBuilder<TranscodeType> applySizing(@NonNull RequestBuilder<TranscodeType> request) {
615 int[] size = new int[2];
616 fillTargetDimensions(size, dimens, bounds);
617 if (size[WIDTH] == 0 && size[HEIGHT] == 0) {
618 size[WIDTH] = getDefaultWidth();
619 size[HEIGHT] = getDefaultHeight();
620 }
621
622 return override(request, size[WIDTH], size[HEIGHT]);
623 }
624
625 private int getDefaultWidth() {
626 ViewGroup.LayoutParams params = getLayoutParams();
627 if (params != null) {
628 return Math.max(params.width, 0);
629 }
630 return 0;
631 }
632
633 private int getDefaultHeight() {
634 ViewGroup.LayoutParams params = getLayoutParams();
635 if (params != null) {
636 return Math.max(params.height, 0);
637 }
638 return 0;
639 }
640
641
642 public interface ThumbnailRequestListener extends RequestListener<Drawable> {
643 void onLoadCanceled();
644
645 void onLoadScheduled();
646 }
647
648 private class ThumbnailClickDispatcher implements View.OnClickListener {
649 @Override
650 public void onClick(View view) {
651 boolean validThumbnail = slide != null &&
652 slide.asAttachment().getUri() != null &&
653 slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE;
654
655 boolean permanentFailure = slide != null && slide.asAttachment().isPermanentlyFailed();
656
657 if (thumbnailClickListener != null && (validThumbnail || permanentFailure)) {
658 thumbnailClickListener.onClick(view, slide);
659 } else if (parentClickListener != null) {
660 parentClickListener.onClick(view);
661 }
662 }
663 }
664
665 private class DownloadClickDispatcher implements View.OnClickListener {
666 @Override
667 public void onClick(View view) {
668 Log.i(TAG, "onClick() for download button");
669 if (startTransferClickListener != null && slide != null) {
670 startTransferClickListener.onClick(view, Collections.singletonList(slide));
671 } else {
672 Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + slide + " downloadClickListener: " + startTransferClickListener);
673 }
674 }
675 }
676
677 private class CancelClickDispatcher implements View.OnClickListener {
678 @Override
679 public void onClick(View view) {
680 Log.i(TAG, "onClick() for cancel button");
681 if (cancelTransferClickListener != null && slide != null) {
682 cancelTransferClickListener.onClick(view, Collections.singletonList(slide));
683 } else {
684 Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelTransferClickListener);
685 }
686 }
687 }
688
689 private class InstantVideoClickDispatcher implements View.OnClickListener {
690 @Override
691 public void onClick(View view) {
692 Log.i(TAG, "onClick() for instant video playback");
693 if (playVideoClickListener != null && slide != null) {
694 playVideoClickListener.onClick(view, slide);
695 } else {
696 Log.w(TAG, "Received an instant video click, but unable to execute it. slide: " + slide + " playVideoClickListener: " + playVideoClickListener);
697 }
698 }
699 }
700
701 private static class BlurHashClearListener implements ListenableFuture.Listener<Boolean> {
702
703 private final RequestManager requestManager;
704 private final ImageView blurHash;
705
706 private BlurHashClearListener(@NonNull RequestManager requestManager, @NonNull ImageView blurHash) {
707 this.requestManager = requestManager;
708 this.blurHash = blurHash;
709 }
710
711 @Override
712 public void onSuccess(Boolean result) {
713 requestManager.clear(blurHash);
714 blurHash.setImageDrawable(null);
715 }
716
717 @Override
718 public void onFailure(ExecutionException e) {
719 requestManager.clear(blurHash);
720 blurHash.setImageDrawable(null);
721 }
722 }
723}