From 07a0bd7bdeed24381a6811a2e523b7ffc891455a Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 10 Feb 2019 12:01:21 +0000 Subject: [PATCH] Improved formatting --- .../eu/faircode/email/ActivityCompose.java | 4 +- .../java/eu/faircode/email/ActivityEml.java | 5 +- .../eu/faircode/email/AdapterMessage.java | 2 +- .../eu/faircode/email/FragmentAnswer.java | 5 +- .../eu/faircode/email/FragmentCompose.java | 35 +++---- .../eu/faircode/email/FragmentIdentity.java | 9 +- .../java/eu/faircode/email/FragmentPro.java | 3 +- .../eu/faircode/email/FragmentQuickSetup.java | 3 +- .../java/eu/faircode/email/HtmlHelper.java | 97 +++++++++++++++---- .../eu/faircode/email/ServiceSynchronize.java | 5 +- 10 files changed, 109 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/ActivityCompose.java b/app/src/main/java/eu/faircode/email/ActivityCompose.java index 01de0feb1b..954a880813 100644 --- a/app/src/main/java/eu/faircode/email/ActivityCompose.java +++ b/app/src/main/java/eu/faircode/email/ActivityCompose.java @@ -22,7 +22,6 @@ package eu.faircode.email; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.view.MenuItem; @@ -131,8 +130,7 @@ public class ActivityCompose extends ActivityBilling implements FragmentManager. CharSequence body = intent.getCharSequenceExtra(Intent.EXTRA_TEXT); if (body != null) if (body instanceof Spanned) - args.putString("body", - Jsoup.clean(Html.toHtml((Spanned) body), Whitelist.relaxed())); + args.putString("body", Jsoup.clean(HtmlHelper.toHtml((Spanned) body), Whitelist.relaxed())); else args.putString("body", body.toString()); // TODO: clean? } diff --git a/app/src/main/java/eu/faircode/email/ActivityEml.java b/app/src/main/java/eu/faircode/email/ActivityEml.java index 9593b6936d..110e66ba3c 100644 --- a/app/src/main/java/eu/faircode/email/ActivityEml.java +++ b/app/src/main/java/eu/faircode/email/ActivityEml.java @@ -5,7 +5,6 @@ import android.content.Context; import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.Bundle; -import android.text.Html; import android.text.Spanned; import android.view.View; import android.widget.TextView; @@ -101,10 +100,10 @@ public class ActivityEml extends ActivityBase { .append(apart.disposition).append(' ') .append(apart.filename); } - result.parts = Html.fromHtml(sb.toString()); + result.parts = HtmlHelper.fromHtml(sb.toString()); String html = HtmlHelper.sanitize(parts.getHtml(context), true); - result.body = Html.fromHtml(html); + result.body = HtmlHelper.fromHtml(html); ByteArrayOutputStream bos = new ByteArrayOutputStream(); mmessage.writeTo(bos); diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 60c33fbce4..138fd2fd0f 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -1547,7 +1547,7 @@ public class AdapterMessage extends RecyclerView.Adapter() { @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index d985defab4..9f72289ddf 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -94,13 +94,11 @@ import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.xml.sax.XMLReader; import java.io.BufferedOutputStream; -import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; -import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; @@ -235,7 +233,7 @@ public class FragmentCompose extends FragmentBase { Spanned signature = null; if (pro) { if (identity != null && !TextUtils.isEmpty(identity.signature)) - signature = Html.fromHtml(identity.signature, new Html.ImageGetter() { + signature = HtmlHelper.fromHtml(identity.signature, new Html.ImageGetter() { @Override public Drawable getDrawable(String source) { int px = Helper.dp2pixels(getContext(), 24); @@ -503,7 +501,7 @@ public class FragmentCompose extends FragmentBase { private void onReferenceEditConfirmed() { Bundle args = new Bundle(); args.putLong("id", working); - args.putString("body", Html.toHtml(etBody.getText())); + args.putString("body", HtmlHelper.toHtml(etBody.getText())); new SimpleTask() { @Override @@ -522,26 +520,23 @@ public class FragmentCompose extends FragmentBase { String body = args.getString("body"); File file = EntityMessage.getFile(context, id); - File ref = EntityMessage.getRefFile(context, id); + File refFile = EntityMessage.getRefFile(context, id); + + String ref = Helper.readText(refFile); + String plain = HtmlHelper.getText(ref); + String html = "

" + plain.replaceAll("\\r?\\n", "
" + "

"); - BufferedReader in = null; BufferedWriter out = null; try { out = new BufferedWriter(new FileWriter(file)); out.write(body); - - in = new BufferedReader(new FileReader(ref)); - String str; - while ((str = in.readLine()) != null) - out.write(str); + out.write(html); } finally { if (out != null) out.close(); - if (in != null) - in.close(); } - ref.delete(); + refFile.delete(); return null; } @@ -1237,8 +1232,8 @@ public class FragmentCompose extends FragmentBase { SpannableString s = new SpannableString(etBody.getText()); ImageSpan is = new ImageSpan(getContext(), Uri.parse("cid:" + BuildConfig.APPLICATION_ID + "." + attachment.id), ImageSpan.ALIGN_BASELINE); s.setSpan(is, start, start + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - String html = Html.toHtml(s); - etBody.setText(Html.fromHtml(html, cidGetter, null)); + String html = HtmlHelper.toHtml(s); + etBody.setText(HtmlHelper.fromHtml(html, cidGetter, null)); } onAction(R.id.action_save); @@ -1295,7 +1290,7 @@ public class FragmentCompose extends FragmentBase { return false; if (!etSubject.getText().toString().trim().equals(etSubject.getTag())) return false; - if (!TextUtils.isEmpty(Jsoup.parse(Html.toHtml(etBody.getText())).text().trim())) + if (!TextUtils.isEmpty(Jsoup.parse(HtmlHelper.toHtml(etBody.getText())).text().trim())) return false; if (rvAttachment.getAdapter().getItemCount() > 0) return false; @@ -1321,7 +1316,7 @@ public class FragmentCompose extends FragmentBase { args.putString("cc", etCc.getText().toString().trim()); args.putString("bcc", etBcc.getText().toString().trim()); args.putString("subject", etSubject.getText().toString().trim()); - args.putString("body", Html.toHtml(spannable)); + args.putString("body", HtmlHelper.toHtml(spannable)); args.putBoolean("empty", isEmpty()); Log.i("Run execute id=" + working); @@ -2253,13 +2248,13 @@ public class FragmentCompose extends FragmentBase { final boolean show_images = args.getBoolean("show_images", false); String body = Helper.readText(EntityMessage.getFile(context, id)); - Spanned spannedBody = Html.fromHtml(body, cidGetter, null); + Spanned spannedBody = HtmlHelper.fromHtml(body, cidGetter, null); Spanned spannedReference = null; File refFile = EntityMessage.getRefFile(context, id); if (refFile.exists()) { String quote = HtmlHelper.sanitize(Helper.readText(refFile), true); - Spanned spannedQuote = Html.fromHtml(quote, + Spanned spannedQuote = HtmlHelper.fromHtml(quote, new Html.ImageGetter() { @Override public Drawable getDrawable(String source) { diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java index 17ff438fc5..c889b15f29 100644 --- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java +++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java @@ -26,7 +26,6 @@ import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.os.Handler; import android.text.Editable; -import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.text.TextWatcher; @@ -296,14 +295,14 @@ public class FragmentIdentity extends FragmentBase { public void onClick(View v) { View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_html, null); final EditText etHtml = dview.findViewById(R.id.etHtml); - etHtml.setText(Html.toHtml(etSignature.getText())); + etHtml.setText(HtmlHelper.toHtml(etSignature.getText())); new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner()) .setView(dview) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - Spanned html = Html.fromHtml(etHtml.getText().toString()); + Spanned html = HtmlHelper.fromHtml(etHtml.getText().toString()); etSignature.setText(html); } }) @@ -480,7 +479,7 @@ public class FragmentIdentity extends FragmentBase { args.putString("password", tilPassword.getEditText().getText().toString()); args.putString("realm", etRealm.getText().toString()); args.putInt("color", color); - args.putString("signature", Html.toHtml(etSignature.getText())); + args.putString("signature", HtmlHelper.toHtml(etSignature.getText())); args.putBoolean("synchronize", cbSynchronize.isChecked()); args.putBoolean("primary", cbPrimary.isChecked()); @@ -718,7 +717,7 @@ public class FragmentIdentity extends FragmentBase { etDisplay.setText(identity == null ? null : identity.display); etSignature.setText(identity == null || - TextUtils.isEmpty(identity.signature) ? null : Html.fromHtml(identity.signature)); + TextUtils.isEmpty(identity.signature) ? null : HtmlHelper.fromHtml(identity.signature)); etHost.setText(identity == null ? null : identity.host); cbStartTls.setChecked(identity == null ? false : identity.starttls); diff --git a/app/src/main/java/eu/faircode/email/FragmentPro.java b/app/src/main/java/eu/faircode/email/FragmentPro.java index d1d49e3e8b..490fb22a23 100644 --- a/app/src/main/java/eu/faircode/email/FragmentPro.java +++ b/app/src/main/java/eu/faircode/email/FragmentPro.java @@ -53,7 +53,8 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha btnPurchase = view.findViewById(R.id.btnPurchase); tvPrice = view.findViewById(R.id.tvPrice); - tvList.setText(Html.fromHtml("" + Html.escapeHtml(getString(R.string.title_pro_list)) + "")); + tvList.setText(HtmlHelper.fromHtml( + "" + Html.escapeHtml(getString(R.string.title_pro_list)) + "")); tvList.setMovementMethod(LinkMovementMethod.getInstance()); btnPurchase.setOnClickListener(new View.OnClickListener() { diff --git a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java index d66a13cdf4..b0749a8b72 100644 --- a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java @@ -34,7 +34,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.Editable; -import android.text.Html; import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; @@ -378,7 +377,7 @@ public class FragmentQuickSetup extends FragmentBase { @Override protected void onException(Bundle args, Throwable ex) { if (args.containsKey("documentation")) { - tvInstructions.setText(Html.fromHtml(args.getString("documentation"))); + tvInstructions.setText(HtmlHelper.fromHtml(args.getString("documentation"))); tvInstructions.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/eu/faircode/email/HtmlHelper.java b/app/src/main/java/eu/faircode/email/HtmlHelper.java index af884f9b0e..aa1bac7218 100644 --- a/app/src/main/java/eu/faircode/email/HtmlHelper.java +++ b/app/src/main/java/eu/faircode/email/HtmlHelper.java @@ -24,6 +24,8 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.text.Html; +import android.text.Spanned; import android.text.TextUtils; import android.util.Base64; @@ -49,20 +51,35 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.text.HtmlCompat; + +import static androidx.core.text.HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM; +import static androidx.core.text.HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE; + public class HtmlHelper { private static final int PREVIEW_SIZE = 250; private static Pattern pattern = Pattern.compile("([http|https]+://[\\w\\S(\\.|:|/)]+)"); - private static final List heads = Arrays.asList("p", "h1", "h2", "h3", "h4", "h5", "tr"); - private static final List tails = Arrays.asList("br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5"); + private static final List heads = Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6", "p", "table", "ol", "ul", "br", "hr"); + private static final List tails = Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6", "p", "ol", "ul", "li"); - static String sanitize(String html, boolean quotes) { + static String sanitize(String html, boolean showQuotes) { Document document = Jsoup.parse(Jsoup.clean(html, Whitelist .relaxed() .addProtocols("img", "src", "cid") .addProtocols("img", "src", "data"))); - for (Element tr : document.select("tr")) - tr.after("
"); + for (Element td : document.select("th,td")) { + Element next = td.nextElementSibling(); + if (next != null && ("th".equals(next.tagName()) || "td".equals(next.tagName()))) + td.append(" "); + else + td.append("
"); + } + + for (Element ol : document.select("ol,ul")) + ol.append("
"); for (Element img : document.select("img")) { boolean linked = false; @@ -88,15 +105,16 @@ public class HtmlHelper { p.appendChild(img); } - if (!quotes) + if (!showQuotes) for (Element quote : document.select("blockquote")) - quote.text("…"); + quote.html("…"); + // Autolink NodeTraversor.traverse(new NodeVisitor() { @Override public void head(Node node, int depth) { if (node instanceof TextNode) { - String text = ((TextNode) node).text(); + String text = Html.escapeHtml(((TextNode) node).text()); Matcher matcher = pattern.matcher(text); while (matcher.find()) { String ref = matcher.group(); @@ -281,29 +299,72 @@ public class HtmlHelper { final StringBuilder sb = new StringBuilder(); NodeTraversor.traverse(new NodeVisitor() { + private int qlevel = 0; + public void head(Node node, int depth) { if (node instanceof TextNode) - sb.append(((TextNode) node).text()); + sb.append(((TextNode) node).text()).append(' '); else { String name = node.nodeName(); - if (name.equals("li")) - sb.append("\n * "); - else if (name.equals("dt")) - sb.append(" "); - else if (heads.contains(name)) - sb.append("\n"); + if ("li".equals(name)) + sb.append("* "); + else if ("blockquote".equals(name)) + qlevel++; + + if (heads.contains(name)) + newline(); } } public void tail(Node node, int depth) { String name = node.nodeName(); + if ("a".equals(name)) + sb.append("[").append(node.absUrl("href")).append("] "); + if ("img".equals(name)) + sb.append("[").append(node.absUrl("src")).append("] "); + else if ("th".equals(name) || "td".equals(name)) { + Node next = node.nextSibling(); + if (next == null || !("th".equals(next.nodeName()) || "td".equals(next.nodeName()))) + newline(); + } else if ("blockquote".equals(name)) + qlevel--; + if (tails.contains(name)) - sb.append("\n"); - else if (name.equals("a")) - sb.append(" <").append(node.absUrl("href")).append(">"); + newline(); + } + + private void newline() { + trimEnd(sb); + sb.append("\n"); + for (int i = 0; i < qlevel; i++) + sb.append('>'); + if (qlevel > 0) + sb.append(' '); } }, Jsoup.parse(html)); + trimEnd(sb); + sb.append("\n"); + return sb.toString(); } + + static void trimEnd(StringBuilder sb) { + int length = sb.length(); + while (length > 0 && sb.charAt(length - 1) == ' ') + length--; + sb.setLength(length); + } + + static Spanned fromHtml(@NonNull String html) { + return fromHtml(html, null, null); + } + + static Spanned fromHtml(@NonNull String html, @Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) { + return HtmlCompat.fromHtml(html, FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM, imageGetter, null); + } + + static String toHtml(Spanned spanned) { + return HtmlCompat.toHtml(spanned, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE); + } } diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index cd08cca975..e554512c19 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -43,7 +43,6 @@ import android.os.Handler; import android.os.PowerManager; import android.os.SystemClock; import android.preference.PreferenceManager; -import android.text.Html; import android.text.TextUtils; import android.util.LongSparseArray; @@ -583,7 +582,7 @@ public class ServiceSynchronize extends LifecycleService { } builder.setStyle(new Notification.BigTextStyle() - .bigText(Html.fromHtml(sb.toString())) + .bigText(HtmlHelper.fromHtml(sb.toString())) .setSummaryText(title)); } @@ -678,7 +677,7 @@ public class ServiceSynchronize extends LifecycleService { if (!TextUtils.isEmpty(message.subject)) sb.append(message.subject).append("
"); sb.append(HtmlHelper.getPreview(body)); - mbuilder.setStyle(new Notification.BigTextStyle().bigText(Html.fromHtml(sb.toString()))); + mbuilder.setStyle(new Notification.BigTextStyle().bigText(HtmlHelper.fromHtml(sb.toString()))); } catch (IOException ex) { Log.e(ex); mbuilder.setStyle(new Notification.BigTextStyle().bigText(ex.toString()));