Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*******************************************************************************
* This file is part of RedReader.
*
* RedReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* RedReader is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with RedReader. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/

package org.quantumbadger.redreader.common;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.util.Log;
import android.view.View;
import android.view.LayoutInflater;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import org.quantumbadger.redreader.R;
import org.quantumbadger.redreader.activities.BaseActivity;
import org.quantumbadger.redreader.account.RedditAccountManager;
import org.quantumbadger.redreader.cache.CacheManager;
import org.quantumbadger.redreader.cache.CacheRequest;
import org.quantumbadger.redreader.cache.CacheRequestCallbacks;
import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategyIfNotCached;
import org.quantumbadger.redreader.common.datastream.SeekableInputStream;
import org.quantumbadger.redreader.common.time.TimestampUTC;
import org.quantumbadger.redreader.reddit.prepared.RedditParsedPost;
import org.quantumbadger.redreader.reddit.prepared.RedditPreparedPost;
import org.quantumbadger.redreader.views.RRAnimationShrinkHeight;
import org.quantumbadger.redreader.views.liststatus.ErrorView;

public class ImagePreviewUtils {

private static final String TAG = "ImagePreviewUtils";
private static final String PROMPT_PREF_KEY = "inline_image_prompt_accepted";
private static final AtomicInteger sInlinePreviewsShownThisSession = new AtomicInteger(0);

public interface ImagePreviewListener {
void setImageBitmap(Bitmap bitmap);
void setLoadingSpinnerVisible(boolean visible);
void setOuterViewVisible(boolean visible);
void setPlayOverlayVisible(boolean visible);
void setPreviewDimensions(String ratio);
LinearLayout getFooterView();
BaseActivity getActivity();
boolean isUsageIdValid(int usageId);
void addErrorView(ErrorView errorView);
void setErrorViewLayout(View errorView);
}

public static void showPrefPrompt(final ImagePreviewListener listener) {
final BaseActivity activity = listener.getActivity();
final SharedPrefsWrapper sharedPrefs = General.getSharedPrefs(activity);

LayoutInflater.from(activity).inflate(
R.layout.inline_images_question_view,
listener.getFooterView(),
true);

final FrameLayout promptView = listener.getFooterView()
.findViewById(R.id.inline_images_prompt_root);
final Button keepShowing = listener.getFooterView()
.findViewById(R.id.inline_preview_prompt_keep_showing_button);
final Button turnOff = listener.getFooterView()
.findViewById(R.id.inline_preview_prompt_turn_off_button);

keepShowing.setOnClickListener(v -> {
new RRAnimationShrinkHeight(promptView).start();
sharedPrefs.edit()
.putBoolean(PROMPT_PREF_KEY, true)
.apply();
});

turnOff.setOnClickListener(v -> {
final String prefPreview = activity.getApplicationContext()
.getString(R.string.pref_images_inline_image_previews_key);
sharedPrefs.edit()
.putBoolean(PROMPT_PREF_KEY, true)
.putString(prefPreview, "never")
.apply();
});
}

public static void downloadInlinePreview(
@NonNull final BaseActivity activity,
@NonNull final RedditPreparedPost post,
@NonNull final ImagePreviewListener listener,
final int usageId) {

final Rect windowVisibleDisplayFrame = DisplayUtils.getWindowVisibleDisplayFrame(activity);

final int screenWidth = Math.min(1080, Math.max(720, windowVisibleDisplayFrame.width()));
final int screenHeight = Math.min(2000, Math.max(400, windowVisibleDisplayFrame.height()));

final RedditParsedPost.ImagePreviewDetails preview = post.src.getPreview(screenWidth, 0);

if(preview == null || preview.width < 10 || preview.height < 10) {
listener.setOuterViewVisible(false);
listener.setLoadingSpinnerVisible(false);
return;
}

final int boundedImageHeight = Math.min(
(screenHeight * 2) / 3,
(int)(((long)preview.height * screenWidth) / preview.width));

listener.setPreviewDimensions(screenWidth + ":" + boundedImageHeight);
listener.setOuterViewVisible(true);
listener.setLoadingSpinnerVisible(true);

CacheManager.getInstance(activity).makeRequest(new CacheRequest(
preview.url,
RedditAccountManager.getAnon(),
null,
new Priority(Constants.Priority.INLINE_IMAGE_PREVIEW),
DownloadStrategyIfNotCached.INSTANCE,
Constants.FileType.INLINE_IMAGE_PREVIEW,
CacheRequest.DownloadQueueType.IMMEDIATE,
activity,
new CacheRequestCallbacks() {
@Override
public void onDataStreamComplete(
@NonNull final GenericFactory<SeekableInputStream, IOException> stream,
final TimestampUTC timestamp,
@NonNull final UUID session,
final boolean fromCache,
@Nullable final String mimetype) {

if(!listener.isUsageIdValid(usageId)) {
return;
}

try(InputStream is = stream.create()) {
final Bitmap data = BitmapFactory.decodeStream(is);

if(data == null) {
throw new IOException("Failed to decode bitmap");
}

// Avoid a crash on badly behaving Android ROMs (where the ImageView
// crashes if an image is too big)
// Should never happen as we limit the preview size to 3000x3000
if(data.getByteCount() > 50 * 1024 * 1024) {
throw new RuntimeException("Image was too large: "
+ data.getByteCount()
+ ", preview URL was "
+ preview.url
+ " and post was "
+ post.src.getIdAndType());
}

final boolean alreadyAcceptedPrompt = General.getSharedPrefs(activity)
.getBoolean(PROMPT_PREF_KEY, false);

final int totalPreviewsShown
= sInlinePreviewsShownThisSession.incrementAndGet();

final boolean isVideoPreview = post.isVideoPreview();

AndroidCommon.runOnUiThread(() -> {
listener.setImageBitmap(data);
listener.setLoadingSpinnerVisible(false);

if(isVideoPreview) {
listener.setPlayOverlayVisible(true);
}

// Show every 8 previews, starting at the second one
if(totalPreviewsShown % 8 == 2 && !alreadyAcceptedPrompt) {
showPrefPrompt(listener);
}
});

} catch(final Throwable t) {
onFailure(General.getGeneralErrorForFailure(
activity,
CacheRequest.RequestFailureType.CONNECTION,
t,
null,
preview.url,
Optional.empty()));
}
}

@Override
public void onFailure(@NonNull final RRError error) {
Log.e(TAG, "Failed to download image preview: " + error, error.t);

if(!listener.isUsageIdValid(usageId)) {
return;
}

AndroidCommon.runOnUiThread(() -> {
listener.setLoadingSpinnerVisible(false);
listener.setOuterViewVisible(false);

final ErrorView errorView = new ErrorView(
activity,
error);

listener.addErrorView(errorView);
listener.setErrorViewLayout(errorView);
});
}
}
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public final class PrefsUtility {
private static SharedPrefsWrapper sharedPrefs;
private static Resources mRes;

public static String getLayoutMode() {
return sharedPrefs.getString("pref_layout_mode", "thumbnails");
}

private static String getPrefKey(@StringRes final int prefKey) {
return mRes.getString(prefKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with RedReader. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/

package org.quantumbadger.redreader.reddit.prepared

import androidx.appcompat.app.AppCompatActivity
Expand Down Expand Up @@ -141,9 +142,25 @@ class RedditParsedPost(
@JvmField val height: Int
)

fun getPreview(minWidth: Int, minHeight: Int) = src.preview?.images?.get(0)?.run {
getPreviewInternal(this, minWidth, minHeight)
}

val isGallery = src.gallery_data?.items?.firstOrNull()?.ok()?.media_id != null

fun getPreview(minWidth: Int, minHeight: Int): ImagePreviewDetails? {
if (isGallery) {
val firstItem = src.gallery_data?.items?.firstOrNull()?.ok()?.media_id ?: return null
val metadata = src.media_metadata?.get(firstItem)?.ok() ?: return null

return ImagePreviewDetails(
UriString(metadata.s.u?.decoded ?: return null),
metadata.s.x.toInt(),
metadata.s.y.toInt()
)
}

return src.preview?.images?.get(0)?.run {
getPreviewInternal(this, minWidth, minHeight)
}
}

fun getPreviewMP4(minWidth: Int, minHeight: Int)
= src.preview?.images?.get(0)?.variants?.mp4?.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public final class RedditPreparedPost implements RedditChangeDataManager.Listene
public final boolean canModerate;
public final boolean hasThumbnail;
public final boolean mIsProbablyAnImage;
public final boolean isGallery;

private final boolean mShowInlinePreviews;

Expand Down Expand Up @@ -109,6 +110,7 @@ public RedditPreparedPost(
isArchived = post.isArchived();
isLocked = post.isLocked();
canModerate = post.getCanModerate();
isGallery = post.isGallery();

mIsProbablyAnImage = LinkHandler.isProbablyAnImage(post.getUrl());

Expand All @@ -127,18 +129,27 @@ public RedditPreparedPost(
}

public boolean shouldShowInlinePreview() {
final String layoutMode = PrefsUtility.getLayoutMode();
final boolean alwaysPreviewMode = layoutMode.equals("always_preview")
|| layoutMode.equals("card");

return mShowInlinePreviews && (src.isPreviewEnabled()
|| "gfycat.com".equals(src.getDomain())
|| "i.imgur.com".equals(src.getDomain())
|| "streamable.com".equals(src.getDomain())
|| "i.redd.it".equals(src.getDomain())
|| "v.redd.it".equals(src.getDomain()));
|| "v.redd.it".equals(src.getDomain())
|| (isGallery && alwaysPreviewMode));
}

public boolean isVideoPreview() {
return src.isVideoPreview();
}

public boolean isGalleryPreview() {
return isGallery;
}

public void performAction(final BaseActivity activity, final RedditPostActions.Action action) {
RedditPostActions.INSTANCE.onActionMenuItemSelected(this, activity, action);
}
Expand Down
Loading
Loading