|
|
@ -21,15 +21,19 @@ package eu.faircode.email;
|
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.SharedPreferences;
|
|
|
|
import android.content.SharedPreferences;
|
|
|
|
|
|
|
|
import android.content.res.Resources;
|
|
|
|
import android.graphics.Bitmap;
|
|
|
|
import android.graphics.Bitmap;
|
|
|
|
import android.graphics.BitmapFactory;
|
|
|
|
import android.graphics.BitmapFactory;
|
|
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
|
|
|
|
|
import android.graphics.drawable.LevelListDrawable;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.net.Uri;
|
|
|
|
|
|
|
|
import android.os.Handler;
|
|
|
|
import android.text.Html;
|
|
|
|
import android.text.Html;
|
|
|
|
import android.text.Spanned;
|
|
|
|
import android.text.Spanned;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Base64;
|
|
|
|
import android.util.Base64;
|
|
|
|
|
|
|
|
import android.widget.TextView;
|
|
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
@ -61,6 +65,8 @@ import java.net.URL;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
|
|
|
|
import java.util.concurrent.Executors;
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
|
|
|
|
|
|
|
|
import static androidx.core.text.HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM;
|
|
|
|
import static androidx.core.text.HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM;
|
|
|
@ -75,6 +81,8 @@ public class HtmlHelper {
|
|
|
|
private static final List<String> tails = Collections.unmodifiableList(Arrays.asList(
|
|
|
|
private static final List<String> tails = Collections.unmodifiableList(Arrays.asList(
|
|
|
|
"h1", "h2", "h3", "h4", "h5", "h6", "p", "ol", "ul", "li"));
|
|
|
|
"h1", "h2", "h3", "h4", "h5", "h6", "p", "ol", "ul", "li"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static final ExecutorService executor = Executors.newCachedThreadPool(Helper.backgroundThreadFactory);
|
|
|
|
|
|
|
|
|
|
|
|
static String removeTracking(Context context, String html) {
|
|
|
|
static String removeTracking(Context context, String html) {
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
boolean paranoid = prefs.getBoolean("paranoid", true);
|
|
|
|
boolean paranoid = prefs.getBoolean("paranoid", true);
|
|
|
@ -298,15 +306,18 @@ public class HtmlHelper {
|
|
|
|
return (body == null ? "" : body.html());
|
|
|
|
return (body == null ? "" : body.html());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static Drawable decodeImage(String source, Context context, long id, boolean show) {
|
|
|
|
static Drawable decodeImage(final String source, long id, boolean show, final TextView view) {
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext());
|
|
|
|
boolean compact = prefs.getBoolean("compact", false);
|
|
|
|
boolean compact = prefs.getBoolean("compact", false);
|
|
|
|
int zoom = prefs.getInt("zoom", compact ? 0 : 1);
|
|
|
|
int zoom = prefs.getInt("zoom", compact ? 0 : 1);
|
|
|
|
|
|
|
|
|
|
|
|
int px = Helper.dp2pixels(context, (zoom + 1) * 24);
|
|
|
|
final int px = Helper.dp2pixels(view.getContext(), (zoom + 1) * 24);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final Resources.Theme theme = view.getContext().getTheme();
|
|
|
|
|
|
|
|
final Resources res = view.getContext().getResources();
|
|
|
|
|
|
|
|
|
|
|
|
if (TextUtils.isEmpty(source)) {
|
|
|
|
if (TextUtils.isEmpty(source)) {
|
|
|
|
Drawable d = context.getResources().getDrawable(R.drawable.baseline_broken_image_24, context.getTheme());
|
|
|
|
Drawable d = res.getDrawable(R.drawable.baseline_broken_image_24, theme);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -320,33 +331,33 @@ public class HtmlHelper {
|
|
|
|
if (!show) {
|
|
|
|
if (!show) {
|
|
|
|
// Show placeholder icon
|
|
|
|
// Show placeholder icon
|
|
|
|
int resid = (embedded || data ? R.drawable.baseline_photo_library_24 : R.drawable.baseline_image_24);
|
|
|
|
int resid = (embedded || data ? R.drawable.baseline_photo_library_24 : R.drawable.baseline_image_24);
|
|
|
|
Drawable d = context.getResources().getDrawable(resid, context.getTheme());
|
|
|
|
Drawable d = res.getDrawable(resid, theme);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Embedded images
|
|
|
|
// Embedded images
|
|
|
|
if (embedded) {
|
|
|
|
if (embedded) {
|
|
|
|
DB db = DB.getInstance(context);
|
|
|
|
DB db = DB.getInstance(view.getContext());
|
|
|
|
String cid = "<" + source.substring(4) + ">";
|
|
|
|
String cid = "<" + source.substring(4) + ">";
|
|
|
|
EntityAttachment attachment = db.attachment().getAttachment(id, cid);
|
|
|
|
EntityAttachment attachment = db.attachment().getAttachment(id, cid);
|
|
|
|
if (attachment == null) {
|
|
|
|
if (attachment == null) {
|
|
|
|
Drawable d = context.getResources().getDrawable(R.drawable.baseline_broken_image_24, context.getTheme());
|
|
|
|
Drawable d = res.getDrawable(R.drawable.baseline_broken_image_24, theme);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
} else if (!attachment.available) {
|
|
|
|
} else if (!attachment.available) {
|
|
|
|
Drawable d = context.getResources().getDrawable(R.drawable.baseline_photo_library_24, context.getTheme());
|
|
|
|
Drawable d = res.getDrawable(R.drawable.baseline_photo_library_24, theme);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
Bitmap bm = Helper.decodeImage(attachment.getFile(context),
|
|
|
|
Bitmap bm = Helper.decodeImage(attachment.getFile(view.getContext()),
|
|
|
|
context.getResources().getDisplayMetrics().widthPixels);
|
|
|
|
res.getDisplayMetrics().widthPixels);
|
|
|
|
if (bm == null) {
|
|
|
|
if (bm == null) {
|
|
|
|
Drawable d = context.getResources().getDrawable(R.drawable.baseline_broken_image_24, context.getTheme());
|
|
|
|
Drawable d = res.getDrawable(R.drawable.baseline_broken_image_24, theme);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
Drawable d = new BitmapDrawable(bm);
|
|
|
|
Drawable d = new BitmapDrawable(res, bm);
|
|
|
|
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
|
|
|
|
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -368,84 +379,110 @@ public class HtmlHelper {
|
|
|
|
if (bm == null)
|
|
|
|
if (bm == null)
|
|
|
|
throw new IllegalArgumentException("decode byte array failed");
|
|
|
|
throw new IllegalArgumentException("decode byte array failed");
|
|
|
|
|
|
|
|
|
|
|
|
Drawable d = new BitmapDrawable(context.getResources(), bm);
|
|
|
|
Drawable d = new BitmapDrawable(res, bm);
|
|
|
|
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
|
|
|
|
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
} catch (IllegalArgumentException ex) {
|
|
|
|
} catch (IllegalArgumentException ex) {
|
|
|
|
Log.w(ex);
|
|
|
|
Log.w(ex);
|
|
|
|
Drawable d = context.getResources().getDrawable(R.drawable.baseline_broken_image_24, context.getTheme());
|
|
|
|
Drawable d = res.getDrawable(R.drawable.baseline_broken_image_24, theme);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get cache file name
|
|
|
|
// Get cache file name
|
|
|
|
File dir = new File(context.getCacheDir(), "images");
|
|
|
|
File dir = new File(view.getContext().getCacheDir(), "images");
|
|
|
|
if (!dir.exists())
|
|
|
|
if (!dir.exists())
|
|
|
|
dir.mkdir();
|
|
|
|
dir.mkdir();
|
|
|
|
File file = new File(dir, id + "_" + Math.abs(source.hashCode()) + ".png");
|
|
|
|
final File file = new File(dir, id + "_" + Math.abs(source.hashCode()) + ".png");
|
|
|
|
|
|
|
|
|
|
|
|
if (file.exists()) {
|
|
|
|
if (file.exists()) {
|
|
|
|
Log.i("Using cached " + file);
|
|
|
|
Log.i("Using cached " + file);
|
|
|
|
Bitmap bm = BitmapFactory.decodeFile(file.getAbsolutePath());
|
|
|
|
Bitmap bm = BitmapFactory.decodeFile(file.getAbsolutePath());
|
|
|
|
if (bm == null) {
|
|
|
|
if (bm == null) {
|
|
|
|
Drawable d = context.getResources().getDrawable(R.drawable.baseline_broken_image_24, context.getTheme());
|
|
|
|
Drawable d = res.getDrawable(R.drawable.baseline_broken_image_24, theme);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
Drawable d = new BitmapDrawable(bm);
|
|
|
|
Drawable d = new BitmapDrawable(res, bm);
|
|
|
|
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
|
|
|
|
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
|
|
|
|
return d;
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
final LevelListDrawable lld = new LevelListDrawable();
|
|
|
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
|
|
Drawable wait = res.getDrawable(R.drawable.baseline_hourglass_empty_24, theme);
|
|
|
|
Log.i("Probe " + source);
|
|
|
|
lld.addLevel(0, 0, wait);
|
|
|
|
try (InputStream probe = new URL(source).openStream()) {
|
|
|
|
lld.setBounds(0, 0, px, px);
|
|
|
|
options.inJustDecodeBounds = true;
|
|
|
|
|
|
|
|
BitmapFactory.decodeStream(probe, null, options);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Log.i("Download " + source);
|
|
|
|
final Handler handler = new Handler(view.getContext().getMainLooper());
|
|
|
|
Bitmap bm;
|
|
|
|
executor.submit(new Runnable() {
|
|
|
|
try (InputStream is = new URL(source).openStream()) {
|
|
|
|
@Override
|
|
|
|
int scaleTo = context.getResources().getDisplayMetrics().widthPixels;
|
|
|
|
public void run() {
|
|
|
|
int factor = 1;
|
|
|
|
try {
|
|
|
|
while (options.outWidth / factor > scaleTo)
|
|
|
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
|
|
factor *= 2;
|
|
|
|
Log.i("Probe " + source);
|
|
|
|
|
|
|
|
try (InputStream probe = new URL(source).openStream()) {
|
|
|
|
if (factor > 1) {
|
|
|
|
options.inJustDecodeBounds = true;
|
|
|
|
Log.i("Download image factor=" + factor);
|
|
|
|
BitmapFactory.decodeStream(probe, null, options);
|
|
|
|
options.inJustDecodeBounds = false;
|
|
|
|
}
|
|
|
|
options.inSampleSize = factor;
|
|
|
|
|
|
|
|
bm = BitmapFactory.decodeStream(is, null, options);
|
|
|
|
|
|
|
|
} else
|
|
|
|
|
|
|
|
bm = BitmapFactory.decodeStream(is);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (bm == null)
|
|
|
|
Log.i("Download " + source);
|
|
|
|
throw new FileNotFoundException("Download image failed");
|
|
|
|
Bitmap bm;
|
|
|
|
|
|
|
|
try (InputStream is = new URL(source).openStream()) {
|
|
|
|
|
|
|
|
int scaleTo = res.getDisplayMetrics().widthPixels;
|
|
|
|
|
|
|
|
int factor = 1;
|
|
|
|
|
|
|
|
while (options.outWidth / factor > scaleTo)
|
|
|
|
|
|
|
|
factor *= 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (factor > 1) {
|
|
|
|
|
|
|
|
Log.i("Download image factor=" + factor);
|
|
|
|
|
|
|
|
options.inJustDecodeBounds = false;
|
|
|
|
|
|
|
|
options.inSampleSize = factor;
|
|
|
|
|
|
|
|
bm = BitmapFactory.decodeStream(is, null, options);
|
|
|
|
|
|
|
|
} else
|
|
|
|
|
|
|
|
bm = BitmapFactory.decodeStream(is);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.i("Downloaded image");
|
|
|
|
if (bm == null)
|
|
|
|
|
|
|
|
throw new FileNotFoundException("Download image failed");
|
|
|
|
|
|
|
|
|
|
|
|
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
|
|
|
|
Log.i("Downloaded image");
|
|
|
|
bm.compress(Bitmap.CompressFormat.PNG, 90, os);
|
|
|
|
|
|
|
|
|
|
|
|
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
|
|
|
|
|
|
|
|
bm.compress(Bitmap.CompressFormat.PNG, 90, os);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create drawable from bitmap
|
|
|
|
|
|
|
|
Drawable d = new BitmapDrawable(res, bm);
|
|
|
|
|
|
|
|
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
|
|
|
|
|
|
|
|
post(d, source);
|
|
|
|
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
|
|
|
|
// Show warning icon
|
|
|
|
|
|
|
|
Log.w(ex);
|
|
|
|
|
|
|
|
int resid = (ex instanceof IOException && !(ex instanceof FileNotFoundException)
|
|
|
|
|
|
|
|
? R.drawable.baseline_cloud_off_24
|
|
|
|
|
|
|
|
: R.drawable.baseline_broken_image_24);
|
|
|
|
|
|
|
|
Drawable d = res.getDrawable(resid, theme);
|
|
|
|
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
|
|
|
|
post(d, source);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create drawable from bitmap
|
|
|
|
private void post(final Drawable d, String source) {
|
|
|
|
Drawable d = new BitmapDrawable(context.getResources(), bm);
|
|
|
|
Log.i("Posting image=" + source);
|
|
|
|
d.setBounds(0, 0, bm.getWidth(), bm.getHeight());
|
|
|
|
handler.post(new Runnable() {
|
|
|
|
return d;
|
|
|
|
@Override
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
public void run() {
|
|
|
|
// Show warning icon
|
|
|
|
lld.addLevel(1, 1, d);
|
|
|
|
Log.w(ex);
|
|
|
|
lld.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
|
|
|
|
int res = (ex instanceof IOException && !(ex instanceof FileNotFoundException)
|
|
|
|
lld.setLevel(1);
|
|
|
|
? R.drawable.baseline_cloud_off_24
|
|
|
|
view.setText(view.getText());
|
|
|
|
: R.drawable.baseline_broken_image_24);
|
|
|
|
}
|
|
|
|
Drawable d = context.getResources().getDrawable(res, context.getTheme());
|
|
|
|
});
|
|
|
|
d.setBounds(0, 0, px, px);
|
|
|
|
}
|
|
|
|
return d;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return lld;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static String getPreview(String body) {
|
|
|
|
static String getPreview(String body) {
|
|
|
|