Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ dependencies {
implementation 'com.mikepenz:aboutlibraries:7.1.0'
implementation 'com.github.di72nn.wallabag-api-wrapper:api-wrapper:v2.0.0-beta.6'
implementation 'org.slf4j:slf4j-android:1.7.36'
implementation 'io.github.panpf.zoomimage:zoomimage-view:1.4.0'
}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
android:name="fr.gaulupeau.apps.Poche.ui.ReadArticleActivity"
android:hardwareAccelerated="true"
android:configChanges="keyboardHidden|orientation|screenSize"/>
<activity
android:name="fr.gaulupeau.apps.Poche.ui.ImageViewActivity"
android:theme="@style/Theme.AppCompat.NoActionBar" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the android:configChanges="keyboardHidden|orientation|screenSize" here, analog to the other activities, so that this image view acitvity does not get recreated when we rotate the phone while viewing the image.

<activity
android:name="fr.gaulupeau.apps.Poche.ui.ManageArticleTagsActivity"
android:label="@string/manageTags_title" />
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/assets/image-zoom-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("click", function(e) {
var target = e.target;
if (target.tagName === "IMG" && target.src) {
e.preventDefault();
e.stopPropagation();
hostImageController.onImageClicked(target.src);
}
}, true);
});
136 changes: 136 additions & 0 deletions app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package fr.gaulupeau.apps.Poche.ui;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;

import androidx.appcompat.app.AppCompatActivity;

import com.github.panpf.zoomimage.ZoomImageView;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;

import fr.gaulupeau.apps.InThePoche.R;
import fr.gaulupeau.apps.Poche.network.ImageCacheUtils;

public class ImageViewActivity extends AppCompatActivity {

public static final String EXTRA_IMAGE_URL = "ImageViewActivity.imageUrl";
public static final String EXTRA_ARTICLE_ID = "ImageViewActivity.articleId";

private static final String TAG = ImageViewActivity.class.getSimpleName();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_view);

ZoomImageView photoView = findViewById(R.id.photoView);
ProgressBar progressBar = findViewById(R.id.progressBar);

String imageUrl = getIntent().getStringExtra(EXTRA_IMAGE_URL);
long articleId = getIntent().getLongExtra(EXTRA_ARTICLE_ID, -1);

if (imageUrl == null || imageUrl.isEmpty()) {
Log.w(TAG, "onCreate() no image URL");
finish();
return;
}

photoView.setOnClickListener(v -> finish());

new Thread(() -> {
Bitmap bitmap = loadBitmap(imageUrl, articleId);
runOnUiThread(() -> {
progressBar.setVisibility(View.GONE);
if (bitmap != null) {
photoView.setImageBitmap(bitmap);
} else {
Log.w(TAG, "onCreate() failed to load image");
finish();
}
});
}).start();
}

private Bitmap loadBitmap(String imageUrl, long articleId) {
// Try loading from local cache first
if (articleId >= 0) {
try {
File file = ImageCacheUtils.getCachedImageFile(imageUrl, articleId);
if (file != null) {
Bitmap bitmap = decodeFileScaled(file.getAbsolutePath());
if (bitmap != null) return bitmap;
}
} catch (Exception e) {
Log.w(TAG, "loadBitmap() cache load failed", e);
}
}

// Try loading from file:// URL (cached images served to WebView)
if (imageUrl.startsWith("file://")) {
try {
String path = imageUrl.substring("file://".length());
Bitmap bitmap = decodeFileScaled(path);
if (bitmap != null) return bitmap;
} catch (Exception e) {
Log.w(TAG, "loadBitmap() file URL load failed", e);
}
}

// Fall back to loading from network
try {
URL url = new URL(imageUrl);
try (InputStream is = url.openStream()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change this to use the OkhttpClient here? WallabagConnection.createClient() could be reused (automatically reusing the app's global network settings).

ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[8192];
int n;
while ((n = is.read(chunk)) != -1) {
buffer.write(chunk, 0, n);
}
return decodeBytesScaled(buffer.toByteArray());
}
} catch (Exception e) {
Log.w(TAG, "loadBitmap() remote load failed", e);
}

return null;
}

// Canvas hardware-accelerated draw limit is ~100MB; cap at 4096px (64MB @ ARGB_8888).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

64 MB is a lot, maybe we need to catch any OOM errors when we use this high resolution? This might be an issue esp. with low-end devices.

private static final int MAX_BITMAP_DIMENSION = 4096;

private static Bitmap decodeFileScaled(String path) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
options.inSampleSize = computeSampleSize(options.outWidth, options.outHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
}

private static Bitmap decodeBytesScaled(byte[] data) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, options);
options.inSampleSize = computeSampleSize(options.outWidth, options.outHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(data, 0, data.length, options);
}

private static int computeSampleSize(int width, int height) {
int sampleSize = 1;
while (width > 0 && height > 0
&& (width / sampleSize > MAX_BITMAP_DIMENSION
|| height / sampleSize > MAX_BITMAP_DIMENSION)) {
sampleSize *= 2;
}
return sampleSize;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.JavascriptInterface;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.FrameLayout;
Expand Down Expand Up @@ -642,6 +643,7 @@ private void initWebView() {

initTtsController();
initAnnotationController();
initImageController();

webViewContent.setWebChromeClient(new WebChromeClient() {
private View customView;
Expand Down Expand Up @@ -800,6 +802,22 @@ private void initTtsController() {
webViewContent.addJavascriptInterface(jsTtsController, "hostWebViewTextController");
}

private void initImageController() {
webViewContent.addJavascriptInterface(new Object() {
@SuppressWarnings("unused")
@JavascriptInterface
public void onImageClicked(String imageUrl) {
Log.d(TAG, "onImageClicked() url: " + imageUrl);
Intent intent = new Intent(ReadArticleActivity.this, ImageViewActivity.class);
intent.putExtra(ImageViewActivity.EXTRA_IMAGE_URL, imageUrl);
if (article != null) {
intent.putExtra(ImageViewActivity.EXTRA_ARTICLE_ID, article.getArticleId().longValue());
}
startActivity(intent);
}
}, "hostImageController");
}

private void initAnnotationController() {
if (!annotationsEnabled) return;

Expand Down Expand Up @@ -918,6 +936,8 @@ private String getHtmlBase() {
private String getExtraHead() {
String extra = "";

extra += "\n\t\t<script src=\"image-zoom-handler.js\"></script>";

if (annotationsEnabled) {
extra += "\n" +
"\t\t<script src=\"annotator.min.js\"></script>" +
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/res/layout/activity_image_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">

<com.github.panpf.zoomimage.ZoomImageView
android:id="@+id/photoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminateTint="@android:color/white" />

</FrameLayout>
1 change: 0 additions & 1 deletion app/src/main/res/values-in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this file did not serve any purpose.

This file was deleted.