org.mozilla.focus.webview.matcher.UrlMatcher.java Source code

Java tutorial

Introduction

Here is the source code for org.mozilla.focus.webview.matcher.UrlMatcher.java

Source

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.focus.webview.matcher;

import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.ArrayMap;
import android.util.JsonReader;

import org.mozilla.focus.R;
import org.mozilla.focus.webview.matcher.util.FocusString;

import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class UrlMatcher implements SharedPreferences.OnSharedPreferenceChangeListener {
    /**
     * Map of pref to blocking category (preference key -> Blocklist category name).
     */
    private final Map<String, String> categoryPrefMap;

    private static final String[] WEBFONT_EXTENSIONS = new String[] { ".woff2", ".woff", ".eot", ".ttf", ".otf" };

    private static final String WEBFONTS = "Webfonts";

    private static Map<String, String> loadDefaultPrefMap(final Context context) {
        Map<String, String> tempMap = new ArrayMap<>();

        tempMap.put(context.getString(R.string.pref_key_privacy_block_ads), "Advertising");
        tempMap.put(context.getString(R.string.pref_key_privacy_block_analytics), "Analytics");
        tempMap.put(context.getString(R.string.pref_key_privacy_block_social), "Social");
        tempMap.put(context.getString(R.string.pref_key_privacy_block_other), "Content");

        // This is a "fake" category - webfont handling is independent of the blocklists
        tempMap.put(context.getString(R.string.pref_key_performance_block_webfonts), WEBFONTS);

        return Collections.unmodifiableMap(tempMap);
    }

    private final Map<String, Trie> categories;
    private final Set<String> enabledCategories = new HashSet<>();

    private final EntityList entityList;
    // A cached list of previously matched URLs. This MUST be cleared whenever items are removed from enabledCategories.
    private final HashSet<String> previouslyMatched = new HashSet<>();
    // A cahced list of previously approved URLs. This MUST be cleared whenever items are added to enabledCategories.
    private final HashSet<String> previouslyUnmatched = new HashSet<>();

    private boolean blockWebfonts = true;

    public static UrlMatcher loadMatcher(final Context context, final int blockListFile,
            final int[] blockListOverrides, final int entityListFile) {
        final Map<String, String> categoryPrefMap = loadDefaultPrefMap(context);

        final Map<String, Trie> categoryMap = new HashMap<>(5);
        try (final JsonReader jsonReader = new JsonReader(new InputStreamReader(
                context.getResources().openRawResource(blockListFile), StandardCharsets.UTF_8))) {
            BlocklistProcessor.loadCategoryMap(jsonReader, categoryMap, BlocklistProcessor.ListType.BASE_LIST);
        } catch (IOException e) {
            throw new IllegalStateException("Unable to parse blacklist");
        }

        if (blockListOverrides != null) {
            for (int i = 0; i < blockListOverrides.length; i++) {
                try (final JsonReader jsonReader = new JsonReader(new InputStreamReader(
                        context.getResources().openRawResource(blockListOverrides[i]), StandardCharsets.UTF_8))) {
                    BlocklistProcessor.loadCategoryMap(jsonReader, categoryMap,
                            BlocklistProcessor.ListType.OVERRIDE_LIST);
                } catch (IOException e) {
                    throw new IllegalStateException("Unable to parse override blacklist");
                }
            }
        }

        final EntityList entityList;
        try (final JsonReader jsonReader = new JsonReader(new InputStreamReader(
                context.getResources().openRawResource(entityListFile), StandardCharsets.UTF_8))) {
            entityList = EntityListProcessor.getEntityMapFromJSON(jsonReader);
        } catch (IOException e) {
            throw new IllegalStateException("Unable to parse entity list");
        }

        return new UrlMatcher(context, categoryPrefMap, categoryMap, entityList);
    }

    /* package-private */ UrlMatcher(final Context context, @NonNull final Map<String, String> categoryPrefMap,
            @NonNull final Map<String, Trie> categoryMap, @Nullable final EntityList entityList) {
        this.categoryPrefMap = categoryPrefMap;
        this.entityList = entityList;
        this.categories = categoryMap;

        // Ensure all categories have been declared, and enable by default (loadPrefs() will then
        // enabled/disable categories that have actually been configured).
        for (final Map.Entry<String, Trie> entry : categoryMap.entrySet()) {
            if (!categoryPrefMap.values().contains(entry.getKey())) {
                throw new IllegalArgumentException("categoryMap contains undeclared category");
            }

            // Failsafe: enable all categories (we load preferences in the next step anyway)
            enabledCategories.add(entry.getKey());
        }

        loadPrefs(context);

        PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String prefName) {
        final String categoryName = categoryPrefMap.get(prefName);

        if (categoryName != null) {
            final boolean prefValue = sharedPreferences.getBoolean(prefName, false);

            setCategoryEnabled(categoryName, prefValue);
        }
    }

    private void loadPrefs(final Context context) {
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

        for (final Map.Entry<String, String> entry : categoryPrefMap.entrySet()) {
            final boolean prefValue = prefs.getBoolean(entry.getKey(), true);
            setCategoryEnabled(entry.getValue(), prefValue);
        }
    }

    @VisibleForTesting
    UrlMatcher(final String[] patterns) {
        final Map<String, String> map = new HashMap<>();
        map.put("default", "default");
        categoryPrefMap = Collections.unmodifiableMap(map);

        categories = new HashMap<>();

        buildMatcher(patterns);

        entityList = null;
    }

    /**
     * Only used for testing - uses a list of urls to populate a "default" category.
     * @param patterns
     */
    private void buildMatcher(String[] patterns) {
        final Trie defaultCategory;
        if (!categories.containsKey("default")) {
            defaultCategory = Trie.createRootNode();
            categories.put("default", defaultCategory);
        } else {
            defaultCategory = categories.get("default");
        }

        for (final String pattern : patterns) {
            defaultCategory.put(FocusString.create(pattern).reverse());
        }

        enabledCategories.add("default");
    }

    public Set<String> getCategories() {
        return categories.keySet();
    }

    public void setCategoryEnabled(final String category, final boolean enabled) {
        if (WEBFONTS.equals(category)) {
            blockWebfonts = enabled;
            return;
        }

        if (!getCategories().contains(category)) {
            throw new IllegalArgumentException("Can't enable/disable inexistant category");
        }

        if (enabled) {
            if (enabledCategories.contains(category)) {
                // Early return - nothing to do if the category is already enabled
                return;
            } else {
                enabledCategories.add(category);
                previouslyUnmatched.clear();
            }
        } else {
            if (!enabledCategories.contains(category)) {
                // Early return - nothing to do if the category is already disabled
                return;
            } else {
                enabledCategories.remove(category);
                previouslyMatched.clear();
            }

        }
    }

    public boolean matches(final Uri resourceURI, final Uri pageURI) {
        final String path = resourceURI.getPath();

        if (path == null) {
            return false;
        }

        // We need to handle webfonts first: if they are blocked, then whitelists don't matter.
        // If they aren't blocked we still need to check domain blacklists below.
        if (blockWebfonts) {
            for (final String extension : WEBFONT_EXTENSIONS) {
                if (path.endsWith(extension)) {
                    return true;
                }
            }
        }

        final String resourceURLString = resourceURI.toString();

        // Cached whitelisted items can be permitted now (but blacklisted needs to wait for the override / entity list)
        if (previouslyUnmatched.contains(resourceURLString)) {
            return false;
        }

        if (entityList != null && entityList.isWhiteListed(pageURI, resourceURI)) {
            // We must not cache entityList items (and/or if we did, we'd have to clear the cache
            // on every single location change)
            return false;
        }

        final String resourceHost = resourceURI.getHost();
        final String pageHost = pageURI.getHost();

        if (pageHost != null && pageHost.equals(resourceHost)) {
            return false;
        }

        if (previouslyMatched.contains(resourceURLString)) {
            return true;
        }

        final FocusString revhost = FocusString.create(resourceHost).reverse();

        for (final Map.Entry<String, Trie> category : categories.entrySet()) {
            if (enabledCategories.contains(category.getKey()) && category.getValue().findNode(revhost) != null) {
                previouslyMatched.add(resourceURLString);
                return true;
            }
        }

        previouslyUnmatched.add(resourceURLString);
        return false;
    }
}