From 4ffed65da3882a0ce1db957555d7efc767288108 Mon Sep 17 00:00:00 2001 From: M66B Date: Tue, 30 Jun 2020 11:19:17 +0200 Subject: [PATCH] Added support for favicons --- FAQ.md | 8 +- .../java/eu/faircode/email/ContactInfo.java | 116 ++++++++++++++++++ .../eu/faircode/email/FragmentOptions.java | 2 +- .../email/FragmentOptionsDisplay.java | 13 +- .../res/layout/fragment_options_display.xml | 25 +++- app/src/main/res/values/strings.xml | 1 + 6 files changed, 158 insertions(+), 7 deletions(-) diff --git a/FAQ.md b/FAQ.md index 6ab419fb90..e632614985 100644 --- a/FAQ.md +++ b/FAQ.md @@ -276,7 +276,7 @@ Fonts, sizes, colors, etc should be material design whenever possible. * [(151) Can you add backup/restore messages?](#user-content-faq151) * [(152) How can I insert a contact group?](#user-content-faq152) * [(153) Why does permanently deleting Gmail message not work?](#user-content-faq153) -* [(154) Can you add favicons as contact photos?](#user-content-faq154) +* [~~(154) Can you add favicons as contact photos?~~](#user-content-faq154) * [(155) What is a winmail.dat file?](#user-content-faq155) * [(156) How can I set up an Office365 account?](#user-content-faq156) * [(157) How can I set up an Free.fr account?](#user-content-faq157) @@ -3045,10 +3045,10 @@ Some background: Gmail seems to have an additional message view for IMAP, which
-**(154) Can you add favicons as contact photos?** +**~~(154) Can you add favicons as contact photos?~~** -Besides that a [favicon](https://en.wikipedia.org/wiki/Favicon) might be shared by many email addresses with the same domain name -and therefore is not directly related to an email address, favicons can be used to track you. +~~Besides that a [favicon](https://en.wikipedia.org/wiki/Favicon) might be shared by many email addresses with the same domain name~~ +~~and therefore is not directly related to an email address, favicons can be used to track you.~~
diff --git a/app/src/main/java/eu/faircode/email/ContactInfo.java b/app/src/main/java/eu/faircode/email/ContactInfo.java index 62d28fdd84..b36f4c6933 100644 --- a/app/src/main/java/eu/faircode/email/ContactInfo.java +++ b/app/src/main/java/eu/faircode/email/ContactInfo.java @@ -36,14 +36,24 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -51,6 +61,7 @@ import java.util.concurrent.ExecutorService; import javax.mail.Address; import javax.mail.internet.InternetAddress; +import javax.net.ssl.HttpsURLConnection; public class ContactInfo { private String email; @@ -63,13 +74,20 @@ public class ContactInfo { private static Map emailLookup = new ConcurrentHashMap<>(); private static final Map emailContactInfo = new HashMap<>(); private static final Map emailGravatar = new HashMap<>(); + private static final List emailFaviconBlacklist = new ArrayList<>(); + private static final ExecutorService executor = Helper.getBackgroundExecutor(1, "contact"); private static final int GRAVATAR_TIMEOUT = 5 * 1000; // milliseconds + private static final int FAVICON_TIMEOUT = 15 * 1000; // milliseconds private static final long CACHE_CONTACT_DURATION = 2 * 60 * 1000L; // milliseconds private static final long CACHE_GRAVATAR_DURATION = 2 * 60 * 60 * 1000L; // milliseconds + static { + emailFaviconBlacklist.add("gmail.com"); + } + private ContactInfo() { } @@ -151,10 +169,12 @@ public class ContactInfo { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean avatars = prefs.getBoolean("avatars", true); boolean gravatars = prefs.getBoolean("gravatars", false); + boolean favicons = prefs.getBoolean("favicons", false); boolean generated = prefs.getBoolean("generated_icons", true); boolean identicons = prefs.getBoolean("identicons", false); boolean circular = prefs.getBoolean("circular", true); + // Contact photo if (!TextUtils.isEmpty(info.email) && Helper.hasPermission(context, Manifest.permission.READ_CONTACTS)) { ContentResolver resolver = context.getContentResolver(); @@ -195,6 +215,7 @@ public class ContactInfo { } } + // Gravatar if (info.bitmap == null) { if (gravatars && !TextUtils.isEmpty(info.email)) { String gkey = info.email.toLowerCase(Locale.ROOT); @@ -243,6 +264,78 @@ public class ContactInfo { } } + // Favicon + if (info.bitmap == null) { + int at = (info.email == null ? -1 : info.email.indexOf('@')); + String domain = (at < 0 ? null : info.email.substring(at + 1).toLowerCase(Locale.ROOT)); + synchronized (emailFaviconBlacklist) { + if (emailFaviconBlacklist.contains(domain)) { + Log.i("Favicon blacklisted domain=" + domain); + domain = null; + } + } + + if (favicons && domain != null) { + try { + File dir = new File(context.getCacheDir(), "favicons"); + if (!dir.exists()) + dir.mkdir(); + File file = new File(dir, domain); + if (file.exists()) + info.bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); + else { + URL base = new URL("https://" + domain); + + info.bitmap = getFavicon(new URL(base, "favicon.ico")); + if (info.bitmap == null) { + Log.i("GET " + base); + HttpsURLConnection connection = (HttpsURLConnection) base.openConnection(); + connection.setRequestMethod("GET"); + connection.setReadTimeout(FAVICON_TIMEOUT); + connection.setConnectTimeout(FAVICON_TIMEOUT); + connection.connect(); + + String response; + try { + response = Helper.readStream(connection.getInputStream(), StandardCharsets.UTF_8.name()); + } finally { + connection.disconnect(); + } + + Document doc = JsoupEx.parse(response); + + Element link = doc.head().select("link[href~=.*\\.(ico|png)]").first(); + String favicon = (link == null ? null : link.attr("href")); + + if (TextUtils.isEmpty(favicon)) { + Element meta = doc.head().select("meta[itemprop=image]").first(); + favicon = (meta == null ? null : meta.attr("content")); + } + + if (!TextUtils.isEmpty(favicon)) { + URL url = new URL(base, favicon); + if ("https".equals(url.getProtocol())) + info.bitmap = getFavicon(url); + } + } + + if (info.bitmap != null) + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + info.bitmap.compress(Bitmap.CompressFormat.PNG, 90, os); + } + } + } catch (Throwable ex) { + Log.w(ex); + } finally { + if (info.bitmap == null) + synchronized (emailFaviconBlacklist) { + emailFaviconBlacklist.add(domain); + } + } + } + } + + // Generated boolean identicon = false; if (info.bitmap == null) { int dp = Helper.dp2pixels(context, 96); @@ -277,6 +370,29 @@ public class ContactInfo { return info; } + private static Bitmap getFavicon(URL url) throws IOException { + try { + Log.i("GET favicon " + url); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setReadTimeout(FAVICON_TIMEOUT); + connection.setConnectTimeout(FAVICON_TIMEOUT); + connection.connect(); + + try { + return BitmapFactory.decodeStream(connection.getInputStream()); + } finally { + connection.disconnect(); + } + } catch (IOException ex) { + Log.w(ex); + if (ex instanceof SocketTimeoutException) + throw ex; + return null; + } + } + static void init(final Context context) { if (Helper.hasPermission(context, Manifest.permission.READ_CONTACTS)) { Handler handler = new Handler(Looper.getMainLooper()); diff --git a/app/src/main/java/eu/faircode/email/FragmentOptions.java b/app/src/main/java/eu/faircode/email/FragmentOptions.java index 3dcb11a60b..3d017d2234 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptions.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptions.java @@ -80,7 +80,7 @@ public class FragmentOptions extends FragmentBase { "subscriptions", "landscape", "landscape3", "startup", "cards", "indentation", "date", "threading", "threading_unread", "highlight_unread", "color_stripe", - "avatars", "gravatars", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold", + "avatars", "gravatars", "favicons", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold", "name_email", "prefer_contact", "distinguish_contacts", "show_recipients", "authentication", "subject_top", "font_size_sender", "font_size_subject", "subject_italic", "highlight_subject", "subject_ellipsize", "keywords_header", "labels_header", "flags", "flags_background", "preview", "preview_italic", "preview_lines", diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java b/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java index 3a5c2787b3..c3739655cc 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java @@ -75,6 +75,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer private SwitchCompat swAvatars; private TextView tvGravatarsHint; private SwitchCompat swGravatars; + private SwitchCompat swFavicons; private SwitchCompat swGeneratedIcons; private SwitchCompat swIdenticons; private SwitchCompat swCircular; @@ -122,7 +123,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer "theme", "startup", "cards", "date", "navbar_colorize", "landscape", "landscape3", "threading", "threading_unread", "indentation", "seekbar", "actionbar", "actionbar_color", "highlight_unread", "color_stripe", - "avatars", "gravatars", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold", + "avatars", "gravatars", "favicons", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold", "name_email", "prefer_contact", "distinguish_contacts", "show_recipients", "subject_top", "font_size_sender", "font_size_subject", "subject_italic", "highlight_subject", "subject_ellipsize", "keywords_header", "labels_header", "flags", "flags_background", @@ -163,6 +164,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer swAvatars = view.findViewById(R.id.swAvatars); swGravatars = view.findViewById(R.id.swGravatars); tvGravatarsHint = view.findViewById(R.id.tvGravatarsHint); + swFavicons = view.findViewById(R.id.swFavicons); swGeneratedIcons = view.findViewById(R.id.swGeneratedIcons); swIdenticons = view.findViewById(R.id.swIdenticons); swCircular = view.findViewById(R.id.swCircular); @@ -344,6 +346,14 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer } }); + swFavicons.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("favicons", checked).apply(); + ContactInfo.clearCache(); + } + }); + tvGravatarsHint.getPaint().setUnderlineText(true); tvGravatarsHint.setOnClickListener(new View.OnClickListener() { @Override @@ -757,6 +767,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer swColorStripe.setChecked(prefs.getBoolean("color_stripe", true)); swAvatars.setChecked(prefs.getBoolean("avatars", true)); swGravatars.setChecked(prefs.getBoolean("gravatars", false)); + swFavicons.setChecked(prefs.getBoolean("favicons", false)); swGeneratedIcons.setChecked(prefs.getBoolean("generated_icons", true)); swIdenticons.setChecked(prefs.getBoolean("identicons", false)); swIdenticons.setEnabled(swGeneratedIcons.isChecked()); diff --git a/app/src/main/res/layout/fragment_options_display.xml b/app/src/main/res/layout/fragment_options_display.xml index 50f102e114..4be2fc4e00 100644 --- a/app/src/main/res/layout/fragment_options_display.xml +++ b/app/src/main/res/layout/fragment_options_display.xml @@ -349,6 +349,29 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/swGravatars" /> + + + + Show color stripe Show contact photos Show Gravatars + Show favicons Show generated icons Show identicons Show round icons