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'
}
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
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:configChanges="keyboardHidden|orientation|screenSize"
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);
});
147 changes: 147 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,147 @@
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.File;

import fr.gaulupeau.apps.InThePoche.R;
import fr.gaulupeau.apps.Poche.network.ImageCacheUtils;
import fr.gaulupeau.apps.Poche.network.WallabagConnection;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

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 {
OkHttpClient client = WallabagConnection.createClient();
Request request = new Request.Builder().url(imageUrl).build();
try (Response response = client.newCall(request).execute()) {
ResponseBody body = response.body();
if (response.isSuccessful() && body != null) {
return decodeBytesScaled(body.bytes());
}
}
} catch (Exception e) {
Log.w(TAG, "loadBitmap() remote load failed", e);
}

return null;
}

// 2048 keeps the bitmap (~16MB @ ARGB_8888) under the Canvas draw limit and
// within GL_MAX_TEXTURE_SIZE on older GPUs. Article images are typically <2048px wide anyway.
private static final int MAX_BITMAP_DIMENSION = 2048;

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;
try {
return BitmapFactory.decodeFile(path, options);
} catch (OutOfMemoryError e) {
Log.w(TAG, "decodeFileScaled() out of memory");
return null;
}
}

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;
try {
return BitmapFactory.decodeByteArray(data, 0, data.length, options);
} catch (OutOfMemoryError e) {
Log.w(TAG, "decodeBytesScaled() out of memory");
return null;
}
}

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>
Loading