That fuck shit the fascists are using
at master 723 lines 27 kB view raw
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}