diff --git a/app/src/main/java/eu/faircode/email/DisconnectBlacklist.java b/app/src/main/java/eu/faircode/email/DisconnectBlacklist.java index 702c90c170..ebe2ceb898 100644 --- a/app/src/main/java/eu/faircode/email/DisconnectBlacklist.java +++ b/app/src/main/java/eu/faircode/email/DisconnectBlacklist.java @@ -45,9 +45,11 @@ import javax.net.ssl.HttpsURLConnection; public class DisconnectBlacklist { private static final Map> map = new HashMap<>(); + private static final List all = new ArrayList<>(); private final static int FETCH_TIMEOUT = 20 * 1000; // milliseconds private final static String LIST = "https://raw.githubusercontent.com/disconnectme/disconnect-tracking-protection/master/services.json"; + final static String URI_CATEGORIES = "https://disconnect.me/trackerprotection#categories-of-trackers"; static void init(Context context) { final File file = getFile(context); @@ -70,6 +72,7 @@ public class DisconnectBlacklist { long start = SystemClock.elapsedRealtime(); map.clear(); + all.clear(); String json = Helper.readText(file); JSONObject jdisconnect = new JSONObject(json); @@ -77,6 +80,7 @@ public class DisconnectBlacklist { Iterator categories = jcategories.keys(); while (categories.hasNext()) { String category = categories.next(); + all.add(category); JSONArray jcategory = jcategories.getJSONArray(category); for (int c = 0; c < jcategory.length(); c++) { JSONObject jblock = (JSONObject) jcategory.get(c); @@ -135,15 +139,34 @@ public class DisconnectBlacklist { init(file); } + static List getCategories() { + synchronized (all) { + return new ArrayList<>(all); + } + } + + static boolean isEnabled(Context context, String category) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean("disconnect_" + category, !"Content".equals(category)); + } + + static void setEnabled(Context context, String category, boolean value) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.edit().putBoolean("disconnect_" + category, value).apply(); + } + static List getCategories(String domain) { return _getCategories(domain); } - static boolean isTracking(String host) { + static boolean isTrackingImage(Context context, String host) { List categories = _getCategories(host); if (categories == null || categories.size() == 0) return false; - return !categories.contains("Content"); + for (String category : categories) + if (isEnabled(context, category)) + return true; + return false; } private static List _getCategories(String domain) { diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsPrivacy.java b/app/src/main/java/eu/faircode/email/FragmentOptionsPrivacy.java index 80d85fa9e6..e4fd9df6b2 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsPrivacy.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsPrivacy.java @@ -36,6 +36,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.Button; +import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageButton; import android.widget.Spinner; @@ -47,11 +48,14 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.SwitchCompat; import androidx.constraintlayout.widget.Group; import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import androidx.webkit.WebViewFeature; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.List; import java.util.Locale; public class FragmentOptionsPrivacy extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -92,6 +96,9 @@ public class FragmentOptionsPrivacy extends FragmentBase implements SharedPrefer private SwitchCompat swDisconnectAutoUpdate; private SwitchCompat swDisconnectLinks; private SwitchCompat swDisconnectImages; + private RecyclerView rvDisconnect; + private ImageButton ibDisconnectCategories; + private AdapterDisconnect adapter; private SwitchCompat swMnemonic; private Button btnClearAll; private TextView tvMnemonic; @@ -158,6 +165,8 @@ public class FragmentOptionsPrivacy extends FragmentBase implements SharedPrefer swDisconnectAutoUpdate = view.findViewById(R.id.swDisconnectAutoUpdate); swDisconnectLinks = view.findViewById(R.id.swDisconnectLinks); swDisconnectImages = view.findViewById(R.id.swDisconnectImages); + rvDisconnect = view.findViewById(R.id.rvDisconnect); + ibDisconnectCategories = view.findViewById(R.id.ibDisconnectCategories); swMnemonic = view.findViewById(R.id.swMnemonic); btnClearAll = view.findViewById(R.id.btnClearAll); tvMnemonic = view.findViewById(R.id.tvMnemonic); @@ -484,6 +493,19 @@ public class FragmentOptionsPrivacy extends FragmentBase implements SharedPrefer @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { prefs.edit().putBoolean("disconnect_images", checked).apply(); + rvDisconnect.setAlpha(checked ? 1.0f : Helper.LOW_LIGHT); + } + }); + + rvDisconnect.setHasFixedSize(false); + rvDisconnect.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new AdapterDisconnect(getContext(), DisconnectBlacklist.getCategories()); + rvDisconnect.setAdapter(adapter); + + ibDisconnectCategories.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.view(v.getContext(), Uri.parse(DisconnectBlacklist.URI_CATEGORIES), true); } }); @@ -626,6 +648,7 @@ public class FragmentOptionsPrivacy extends FragmentBase implements SharedPrefer swDisconnectAutoUpdate.setChecked(prefs.getBoolean("disconnect_auto_update", false)); swDisconnectLinks.setChecked(prefs.getBoolean("disconnect_links", true)); swDisconnectImages.setChecked(prefs.getBoolean("disconnect_images", false)); + rvDisconnect.setAlpha(swDisconnectImages.isChecked() ? 1.0f : Helper.LOW_LIGHT); String mnemonic = prefs.getString("wipe_mnemonic", null); swMnemonic.setChecked(mnemonic != null); @@ -634,4 +657,70 @@ public class FragmentOptionsPrivacy extends FragmentBase implements SharedPrefer Log.e(ex); } } + + public static class AdapterDisconnect extends RecyclerView.Adapter { + private Context context; + private LayoutInflater inflater; + + private List items; + + public class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener { + private CheckBox cbEnabled; + + ViewHolder(View itemView) { + super(itemView); + cbEnabled = itemView.findViewById(R.id.cbEnabled); + } + + private void wire() { + cbEnabled.setOnCheckedChangeListener(this); + } + + private void unwire() { + cbEnabled.setOnCheckedChangeListener(null); + } + + private void bindTo(String category) { + cbEnabled.setText(category); + cbEnabled.setChecked(DisconnectBlacklist.isEnabled(context, category)); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + int pos = getAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) + return; + + String category = items.get(pos); + DisconnectBlacklist.setEnabled(context, category, isChecked); + } + } + + AdapterDisconnect(Context context, List items) { + this.context = context; + this.inflater = LayoutInflater.from(context); + + setHasStableIds(false); + this.items = items; + } + + @Override + public int getItemCount() { + return items.size(); + } + + @Override + @NonNull + public AdapterDisconnect.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new AdapterDisconnect.ViewHolder(inflater.inflate(R.layout.item_disconnect_enabled, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull AdapterDisconnect.ViewHolder holder, int position) { + holder.unwire(); + String category = items.get(position); + holder.bindTo(category); + holder.wire(); + } + } } diff --git a/app/src/main/java/eu/faircode/email/HtmlHelper.java b/app/src/main/java/eu/faircode/email/HtmlHelper.java index eedfe7dbfc..819ba4b2cb 100644 --- a/app/src/main/java/eu/faircode/email/HtmlHelper.java +++ b/app/src/main/java/eu/faircode/email/HtmlHelper.java @@ -57,7 +57,6 @@ import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Base64; -import android.util.Pair; import android.util.Patterns; import android.view.View; @@ -2287,7 +2286,7 @@ public class HtmlHelper { Uri uri = Uri.parse(img.attr("src")); String host = uri.getHost(); if (host != null && !hosts.contains(host) && - !isTrackingHost(host, disconnect_images)) + !isTrackingHost(context, host, disconnect_images)) hosts.add(host); } } @@ -2305,7 +2304,7 @@ public class HtmlHelper { if (host == null || hosts.contains(host)) continue; - if (isTrackingPixel(img) || isTrackingHost(host, disconnect_images)) { + if (isTrackingPixel(img) || isTrackingHost(context, host, disconnect_images)) { img.attr("src", sb.toString()); img.attr("alt", context.getString(R.string.title_legend_tracking_pixel)); img.attr("height", "24"); @@ -2340,10 +2339,10 @@ public class HtmlHelper { } } - private static boolean isTrackingHost(String host, boolean disconnect_images) { + private static boolean isTrackingHost(Context context, String host, boolean disconnect_images) { if (TRACKING_HOSTS.contains(host)) return true; - if (disconnect_images && DisconnectBlacklist.isTracking(host)) + if (disconnect_images && DisconnectBlacklist.isTrackingImage(context, host)) return true; return false; } diff --git a/app/src/main/res/layout/fragment_options_privacy.xml b/app/src/main/res/layout/fragment_options_privacy.xml index f4a6ec6892..2ab617a882 100644 --- a/app/src/main/res/layout/fragment_options_privacy.xml +++ b/app/src/main/res/layout/fragment_options_privacy.xml @@ -691,6 +691,29 @@ app:layout_constraintTop_toBottomOf="@+id/swDisconnectLinks" app:switchPadding="12dp" /> + + + +