org.adblockplus.android.ProxyService.java Source code

Java tutorial

Introduction

Here is the source code for org.adblockplus.android.ProxyService.java

Source

/*
 * This file is part of Adblock Plus <http://adblockplus.org/>,
 * Copyright (C) 2006-2013 Eyeo GmbH
 *
 * Adblock Plus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * Adblock Plus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.adblockplus.android;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeoutException;

import sunlabs.brazil.server.Server;
import sunlabs.brazil.util.Base64;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Proxy;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.StrictMode;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.stericson.RootTools.RootTools;
import com.stericson.RootTools.RootToolsException;

public class ProxyService extends Service implements OnSharedPreferenceChangeListener {
    private static final String LOCALHOST = "127.0.0.1";
    /**
     * Indicates that system supports native proxy configuration.
     */
    public static final boolean NATIVE_PROXY_SUPPORTED = Build.VERSION.SDK_INT >= 12; // Honeycomb 3.1

    static {
        RootTools.debugMode = false;
    }

    private static final String TAG = "ProxyService";
    private static final boolean logRequests = false;

    // Do not use 8080 because it is a "dirty" port, Android uses it if something goes wrong
    // First element is reserved for previously used port
    private static final int[] portVariants = new int[] { -1, 2020, 3030, 4040, 5050, 6060, 7070, 9090, 1234, 12345,
            4321, 0 };

    private final static int DEFAULT_TIMEOUT = 3000;
    private final static int NO_TRAFFIC_TIMEOUT = 5 * 60 * 1000; // 5 minutes

    final static int ONGOING_NOTIFICATION_ID = R.string.app_name;
    private static final long POSITION_RIGHT = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD
            ? Long.MIN_VALUE
            : Long.MAX_VALUE;
    private final static int NOTRAFFIC_NOTIFICATION_ID = R.string.app_name + 3;

    /**
     * Broadcasted when service starts or stops.
     */
    public final static String BROADCAST_STATE_CHANGED = "org.adblockplus.android.service.state";
    /**
     * Broadcasted if proxy fails to start.
     */
    public final static String BROADCAST_PROXY_FAILED = "org.adblockplus.android.proxy.failure";

    private final static String IPTABLES_RETURN = " -t nat -m owner --uid-owner {{UID}} -A OUTPUT -p tcp -j RETURN\n";
    private final static String IPTABLES_ADD_HTTP = " -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to {{PORT}}\n";

    boolean hideIcon;
    private Handler notrafficHandler;

    protected ProxyServer proxy = null;
    protected int port;
    private Properties proxyConfiguration = new Properties();

    /**
     * Indicates that service is working with root privileges.
     */
    private boolean transparent = false;
    /**
     * Indicates that service has autoconfigured Android proxy settings (version 3.1+).
     */
    private boolean nativeProxyAutoConfigured = false;
    /**
     * Indicates that Android proxy settings are correctly configured (version 4.1.2+ 4.2.2+).
     */
    private boolean proxyManualyConfigured = false;

    private String iptables = null;

    @SuppressLint("NewApi")
    @Override
    public void onCreate() {
        super.onCreate();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            // Proxy is running in separate thread, it's just some resolution request during initialization.
            // Not worth spawning a separate thread for this.
            StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitNetwork().build();
            StrictMode.setThreadPolicy(policy);
        }

        // Get port for local proxy
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        Resources resources = getResources();

        // Try to read user proxy settings
        String proxyHost = null;
        String proxyPort = null;
        String proxyExcl = null;
        String proxyUser = null;
        String proxyPass = null;

        if (NATIVE_PROXY_SUPPORTED) {
            // Read system settings
            proxyHost = System.getProperty("http.proxyHost");
            proxyPort = System.getProperty("http.proxyPort");
            proxyExcl = System.getProperty("http.nonProxyHosts");

            Log.d(TAG, "PRX: " + proxyHost + ":" + proxyPort + "(" + proxyExcl + ")");
            // not used but left for future reference
            String[] px = ProxySettings.getUserProxy(getApplicationContext());
            if (px != null)
                Log.d(TAG, "PRX: " + px[0] + ":" + px[1] + "(" + px[2] + ")");
        } else {
            // Read application settings
            proxyHost = prefs.getString(getString(R.string.pref_proxyhost), null);
            proxyPort = prefs.getString(getString(R.string.pref_proxyport), null);
            proxyUser = prefs.getString(getString(R.string.pref_proxyuser), null);
            proxyPass = prefs.getString(getString(R.string.pref_proxypass), null);
        }

        // Check for root privileges and try to install transparent proxy
        if (RootTools.isAccessGiven()) {
            try {
                initIptables();

                StringBuffer cmd = new StringBuffer();
                int uid = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.uid;
                cmd.append(iptables);
                cmd.append(IPTABLES_RETURN.replace("{{UID}}", String.valueOf(uid)));
                String rules = cmd.toString();
                RootTools.sendShell(rules, DEFAULT_TIMEOUT);
                transparent = true;
            } catch (FileNotFoundException e) {
                // ignore - this is "normal" case
            } catch (NameNotFoundException e) {
                Log.e(TAG, "Failed to initialize iptables", e);
            } catch (IOException e) {
                Log.e(TAG, "Failed to initialize iptables", e);
            } catch (RootToolsException e) {
                Log.e(TAG, "Failed to initialize iptables", e);
            } catch (TimeoutException e) {
                Log.e(TAG, "Failed to initialize iptables", e);
            }
        }

        if (!transparent) {
            // Try to set native proxy
            nativeProxyAutoConfigured = ProxySettings.setConnectionProxy(getApplicationContext(), LOCALHOST, port,
                    "");

            if (NATIVE_PROXY_SUPPORTED) {
                registerReceiver(connectionReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
                registerReceiver(connectionReceiver, new IntentFilter(Proxy.PROXY_CHANGE_ACTION));
            }
        }

        // Save current native proxy situation. The service is always started on the first run so
        // we will always have a correct value from the box
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean(getString(R.string.pref_proxyautoconfigured), transparent || nativeProxyAutoConfigured);
        editor.commit();

        registerReceiver(proxyReceiver, new IntentFilter(ProxyService.BROADCAST_PROXY_FAILED));
        registerReceiver(filterReceiver, new IntentFilter(AdblockPlus.BROADCAST_FILTERING_CHANGE));
        registerReceiver(filterReceiver, new IntentFilter(AdblockPlus.BROADCAST_FILTER_MATCHES));

        // Start proxy
        if (proxy == null) {
            // Select available port and bind to it, use previously selected port by default
            portVariants[0] = prefs.getInt(getString(R.string.pref_lastport), -1);
            ServerSocket listen = null;
            String msg = null;
            for (int p : portVariants) {
                if (p < 0)
                    continue;
                try {
                    listen = new ServerSocket(p, 1024);
                    port = p;
                    break;
                } catch (IOException e) {
                    Log.e(TAG, null, e);
                    msg = e.getMessage();
                }
            }
            if (listen == null) {
                sendBroadcast(new Intent(BROADCAST_PROXY_FAILED).putExtra("msg", msg));
                return;
            }

            // Save selected port
            editor.putInt(getString(R.string.pref_lastport), port);
            editor.commit();

            // Initialize proxy
            proxyConfiguration.put("handler", "main");
            proxyConfiguration.put("main.prefix", "");
            proxyConfiguration.put("main.class", "sunlabs.brazil.server.ChainHandler");
            if (transparent) {
                proxyConfiguration.put("main.handlers", "urlmodifier adblock");
                proxyConfiguration.put("urlmodifier.class", "org.adblockplus.brazil.TransparentProxyHandler");
            } else {
                proxyConfiguration.put("main.handlers", "https adblock");
                proxyConfiguration.put("https.class", "org.adblockplus.brazil.SSLConnectionHandler");
            }
            proxyConfiguration.put("adblock.class", "org.adblockplus.brazil.RequestHandler");
            if (logRequests)
                proxyConfiguration.put("adblock.proxylog", "yes");

            configureUserProxy(proxyConfiguration, proxyHost, proxyPort, proxyExcl, proxyUser, proxyPass);

            proxy = new ProxyServer();
            proxy.logLevel = Server.LOG_DIAGNOSTIC;
            proxy.setup(listen, proxyConfiguration.getProperty("handler"), proxyConfiguration);
            proxy.start();
        }

        if (transparent) {
            // Redirect traffic via iptables
            try {
                StringBuffer cmd = new StringBuffer();
                cmd.append(iptables);
                cmd.append(IPTABLES_ADD_HTTP.replace("{{PORT}}", String.valueOf(port)));
                String rules = cmd.toString();
                RootTools.sendShell(rules, DEFAULT_TIMEOUT);
            } catch (FileNotFoundException e) {
                // ignore - this is "normal" case
            } catch (IOException e) {
                Log.e(TAG, "Failed to initialize iptables", e);
            } catch (RootToolsException e) {
                Log.e(TAG, "Failed to initialize iptables", e);
            } catch (TimeoutException e) {
                Log.e(TAG, "Failed to initialize iptables", e);
            }
        }

        prefs.registerOnSharedPreferenceChangeListener(this);

        // Lock service
        hideIcon = prefs.getBoolean(getString(R.string.pref_hideicon), resources.getBoolean(R.bool.def_hideicon));
        startForeground(ONGOING_NOTIFICATION_ID, getNotification());

        // If automatic setting of proxy was blocked, check if user has set it manually
        boolean manual = isManual();
        if (manual && NATIVE_PROXY_SUPPORTED) {
            ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(
                    Context.CONNECTIVITY_SERVICE);
            updateNoTrafficCheck(connectivityManager);
        }

        sendStateChangedBroadcast();
        Log.i(TAG, "Service started");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        stopNoTrafficCheck();

        unregisterReceiver(filterReceiver);
        unregisterReceiver(proxyReceiver);

        // Stop IP redirecting
        if (transparent) {
            new Thread() {
                @Override
                public void run() {
                    try {
                        RootTools.sendShell(iptables + " -t nat -F OUTPUT", DEFAULT_TIMEOUT);
                    } catch (Exception e) {
                        Log.e(TAG, "Failed to clear iptables", e);
                    }
                }
            }.start();
        }

        if (!transparent && NATIVE_PROXY_SUPPORTED)
            unregisterReceiver(connectionReceiver);

        // Clear native proxy
        if (nativeProxyAutoConfigured) {
            clearConnectionProxy();
        }

        sendBroadcast(new Intent(BROADCAST_STATE_CHANGED).putExtra("enabled", false));

        // Stop proxy server
        if (proxy != null)
            proxy.close();

        // TODO Do we have to check current state?
        // Stop engine if not in interactive mode
        AdblockPlus.getApplication().stopEngine(false);

        // Release service lock
        stopForeground(true);

        Log.i(TAG, "Service stopped");
    }

    /**
     * Restores system proxy settings via native call on Android 3.1+ devices
     * using Java reflection.
     */
    private void clearConnectionProxy() {
        String proxyHost = (String) proxyConfiguration.getProperty("adblock.proxyHost");
        String proxyPort = (String) proxyConfiguration.getProperty("adblock.proxyPort");
        String proxyExcl = (String) proxyConfiguration.getProperty("adblock.proxyExcl");
        int port = 0;
        try {
            if (proxyHost != null)
                port = Integer.valueOf(proxyPort);
        } catch (NumberFormatException e) {
            Log.e(TAG, "Bad port setting", e);
        }
        ProxySettings.setConnectionProxy(getApplicationContext(), proxyHost, port, proxyExcl);
    }

    /**
     * Sets user proxy settings in proxy service properties.
     */
    private void configureUserProxy(Properties config, String proxyHost, String proxyPort, String proxyExcl,
            String proxyUser, String proxyPass) {
        // Clean previous settings
        config.remove("adblock.proxyHost");
        config.remove("adblock.proxyPort");
        config.remove("adblock.auth");
        config.remove("adblock.proxyExcl");
        if (!transparent) {
            config.remove("https.proxyHost");
            config.remove("https.proxyPort");
            config.remove("https.auth");
        }

        if (nativeProxyAutoConfigured)
            passProxySettings(proxyHost, proxyPort, proxyExcl);

        // Check if there are any settings
        if (proxyHost == null || "".equals(proxyHost))
            return;

        // Check for dirty proxy settings - this indicated previous crash:
        // proxy points to ourselves
        // proxy port is null, 0 or not a number
        // proxy is 127.0.0.1:8080
        if (proxyPort == null)
            return;
        int p = 0;
        try {
            p = Integer.valueOf(proxyPort);
        } catch (NumberFormatException e) {
            return;
        }
        if (p == 0 || isLocalHost(proxyHost) && (p == port || p == 8080)) {
            if (nativeProxyAutoConfigured)
                passProxySettings(null, null, null);
            return;
        }

        config.put("adblock.proxyHost", proxyHost);
        config.put("adblock.proxyPort", proxyPort);
        if (!transparent) {
            config.put("https.proxyHost", proxyHost);
            config.put("https.proxyPort", proxyPort);
        }

        // TODO Not implemented in our proxy but needed to restore settings
        if (proxyExcl != null)
            config.put("adblock.proxyExcl", proxyExcl);

        if (proxyUser != null && !"".equals(proxyUser) && proxyPass != null && !"".equals(proxyPass)) {
            // Base64 encode user:password
            String proxyAuth = "Basic " + new String(Base64.encode(proxyUser + ":" + proxyPass));
            config.put("adblock.auth", proxyAuth);
            if (!transparent)
                config.put("https.auth", proxyAuth);
        }
    }

    private void passProxySettings(String proxyHost, String proxyPort, String proxyExcl) {
        try {
            CrashHandler handler = (CrashHandler) Thread.getDefaultUncaughtExceptionHandler();
            handler.saveProxySettings(proxyHost, proxyPort, proxyExcl);
        } catch (ClassCastException e) {
            // ignore - default handler in use
        }
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (!NATIVE_PROXY_SUPPORTED) {
            String ketHost = getString(R.string.pref_proxyhost);
            String keyPort = getString(R.string.pref_proxyport);
            String keyUser = getString(R.string.pref_proxyuser);
            String keyPass = getString(R.string.pref_proxypass);
            if (key.equals(ketHost) || key.equals(keyPort) || key.equals(keyUser) || key.equals(keyPass)) {
                String proxyHost = sharedPreferences.getString(ketHost, null);
                String proxyPort = sharedPreferences.getString(keyPort, null);
                String proxyUser = sharedPreferences.getString(keyUser, null);
                String proxyPass = sharedPreferences.getString(keyPass, null);
                if (proxy != null) {
                    configureUserProxy(proxyConfiguration, proxyHost, proxyPort, null, proxyUser, proxyPass);
                    proxy.restart(proxyConfiguration.getProperty("handler"));
                }
            }
        }
    }

    public boolean isTransparent() {
        return transparent;
    }

    public boolean isNativeProxyAutoConfigured() {
        return nativeProxyAutoConfigured;
    }

    /**
     * Checks if user has to set proxy settings manually
     */
    public boolean isManual() {
        return !transparent && !nativeProxyAutoConfigured;
    }

    /**
     * Checks whether traffic check is pending
     */
    public boolean noTraffic() {
        return notrafficHandler != null;
    }

    /**
     * Checks if specified host is local.
     */
    private static final boolean isLocalHost(String host) {
        if (host == null)
            return false;

        try {
            if (host.equalsIgnoreCase("localhost"))
                return true;

            String className = "android.net.NetworkUtils";
            Class<?> c = Class.forName(className);
            /*
             * InetAddress address = NetworkUtils.numericToInetAddress(host);
             */
            Method method = c.getMethod("numericToInetAddress", String.class);
            InetAddress address = (InetAddress) method.invoke(null, host);

            if (address.isLoopbackAddress())
                return true;
        } catch (Exception e) {
            Log.w(TAG, null, e);
        }
        return false;
    }

    /**
     * Initializes iptables executable.
     * 
     * @throws FileNotFoundException
     *           If iptables initialization failed due to provided reasons.
     */
    private void initIptables() throws IOException, RootToolsException, TimeoutException, FileNotFoundException {
        if (!RootTools.isAccessGiven())
            throw new FileNotFoundException("No root access");

        File ipt = getFileStreamPath("iptables");

        if (!ipt.exists()) {
            Log.e(TAG, "No iptables excutable found");
            throw new FileNotFoundException("No iptables executable");
        }

        String path = ipt.getAbsolutePath();

        RootTools.sendShell("chmod 700 " + path, DEFAULT_TIMEOUT);

        boolean compatible = false;
        boolean version = false;

        String command = path + " --version\n" + path + " -L -t nat -n\n";

        List<String> result = RootTools.sendShell(command, DEFAULT_TIMEOUT);
        for (String line : result) {
            if (line.contains("OUTPUT"))
                compatible = true;
            if (line.contains("v1.4."))
                version = true;
        }

        if (!compatible || !version) {
            Log.e(TAG, "Incompatible iptables excutable");
            throw new FileNotFoundException("Incompatible iptables excutable");
        }

        iptables = path;
    }

    public List<String> getIptablesOutput() {
        if (iptables == null)
            return null;

        String command = iptables + " -L -t nat -n\n";
        try {
            return RootTools.sendShell(command, DEFAULT_TIMEOUT);
        } catch (Exception e) {
            Log.e(TAG, "Failed to get iptables configuration", e);
            return null;
        }
    }

    /**
     * Raises or removes no traffic notification based on current link proxy
     * settings
     */
    private void updateNoTrafficCheck(ConnectivityManager connectivityManager) {
        try {
            Object pp = ProxySettings.getActiveLinkProxy(connectivityManager);
            String[] userProxy = ProxySettings.getUserProxy(pp);
            if (userProxy != null)
                Log.i(TAG, "Proxy settings: " + userProxy[0] + ":" + userProxy[1] + "(" + userProxy[2] + ")");
            updateNoTrafficCheck(userProxy);
        } catch (Exception e) {
            // This should not happen
            Log.e(TAG, null, e);
        }
    }

    /**
     * Raises or removes no traffic notification based on the user proxy settings
     */
    private void updateNoTrafficCheck(String[] userProxy) {
        boolean ourProxy = userProxy != null && isLocalHost(userProxy[0]) && Integer.valueOf(userProxy[1]) == port;
        if (ourProxy != proxyManualyConfigured) {
            proxyManualyConfigured = ourProxy;
            sendStateChangedBroadcast();
        }
        if (ourProxy) {
            stopNoTrafficCheck();
        } else {
            // Initiate no traffic check
            notrafficHandler = new Handler();
            notrafficHandler.postDelayed(noTraffic, NO_TRAFFIC_TIMEOUT);
        }
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(ONGOING_NOTIFICATION_ID, getNotification());
    }

    /**
     * Stops no traffic check and resets notification message.
     */
    private void stopNoTrafficCheck() {
        if (notrafficHandler != null) {
            notrafficHandler.removeCallbacks(noTraffic);
            sendStateChangedBroadcast();
            NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            notificationManager.notify(ONGOING_NOTIFICATION_ID, getNotification());
            notificationManager.cancel(NOTRAFFIC_NOTIFICATION_ID);
        }
        notrafficHandler = null;
    }

    @SuppressLint("NewApi")
    private Notification getNotification() {
        boolean filtering = AdblockPlus.getApplication().isFilteringEnabled();

        int msgId = R.string.notif_waiting;
        if (nativeProxyAutoConfigured || proxyManualyConfigured)
            msgId = filtering ? R.string.notif_wifi : R.string.notif_wifi_nofiltering;
        if (transparent)
            msgId = R.string.notif_all;

        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        if (hideIcon && msgId != R.string.notif_waiting) {
            builder.setWhen(POSITION_RIGHT);
            builder.setSmallIcon(R.drawable.transparent);
            //builder.setContent(new RemoteViews(getPackageName(), R.layout.notif_hidden));
        } else {
            builder.setWhen(0);
            builder.setSmallIcon(R.drawable.ic_stat_blocking);
        }
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, Preferences.class)
                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK), 0);
        builder.setContentIntent(contentIntent);
        builder.setContentTitle(getText(R.string.app_name));
        builder.setContentText(getString(msgId, port));
        builder.setOngoing(true);

        Notification notification = builder.getNotification();
        return notification;
    }

    public void setEmptyIcon(boolean hide) {
        hideIcon = hide;
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(ONGOING_NOTIFICATION_ID, getNotification());
    }

    public void sendStateChangedBroadcast() {
        Log.i(TAG, "Broadcasting " + BROADCAST_STATE_CHANGED);
        boolean manual = isManual();
        Intent stateIntent = new Intent(BROADCAST_STATE_CHANGED).putExtra("enabled", true).putExtra("port", port)
                .putExtra("manual", manual);
        if (manual)
            stateIntent.putExtra("configured", proxyManualyConfigured);
        sendBroadcast(stateIntent);
    }

    private final IBinder binder = new LocalBinder();

    public final class LocalBinder extends Binder {
        public ProxyService getService() {
            return ProxyService.this;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    /**
     * Executed if no traffic is detected after a period of time. Notifies user
     * about possible configuration problems.
     */
    private Runnable noTraffic = new Runnable() {
        public void run() {
            // It's weird but notrafficHandler.removeCallbacks(noTraffic) does not remove this callback
            if (notrafficHandler == null)
                return;
            // Show warning notification
            NotificationCompat.Builder builder = new NotificationCompat.Builder(ProxyService.this);
            builder.setSmallIcon(R.drawable.ic_stat_warning);
            builder.setWhen(System.currentTimeMillis());
            builder.setAutoCancel(true);
            Intent intent = new Intent(ProxyService.this, ConfigurationActivity.class)
                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.putExtra("port", port);
            PendingIntent contentIntent = PendingIntent.getActivity(ProxyService.this, 0, intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            builder.setContentIntent(contentIntent);
            builder.setContentTitle(getText(R.string.app_name));
            builder.setContentText(getText(R.string.notif_notraffic));
            NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            notificationManager.notify(NOTRAFFIC_NOTIFICATION_ID, builder.getNotification());
        }
    };

    /**
     * Stops no traffic check if traffic is detected by proxy service.
     */
    private BroadcastReceiver filterReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, Intent intent) {
            if (intent.getAction().equals(AdblockPlus.BROADCAST_FILTERING_CHANGE)) {
                // It's rather a hack but things are happening simultaneously and we
                // receive this broadcast despite the fact we have unsubscribed from
                // it and notification is not removed because it is changed to new one
                // during removal.
                if (!ProxyService.this.isNativeProxyAutoConfigured()) {
                    NotificationManager notificationManager = (NotificationManager) getSystemService(
                            NOTIFICATION_SERVICE);
                    notificationManager.notify(ONGOING_NOTIFICATION_ID, getNotification());
                }
            }
            if (intent.getAction().equals(AdblockPlus.BROADCAST_FILTER_MATCHES)) {
                proxyManualyConfigured = true;
                stopNoTrafficCheck();
            }
        }
    };

    /**
     * Stops service if proxy fails.
     */
    private BroadcastReceiver proxyReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, Intent intent) {
            if (intent.getAction().equals(ProxyService.BROADCAST_PROXY_FAILED)) {
                stopSelf();
            }
        }
    };

    /**
     * Monitors system network connection settings changes and updates proxy
     * settings accordingly.
     */
    private BroadcastReceiver connectionReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context ctx, Intent intent) {
            String action = intent.getAction();
            Log.i(TAG, "Action: " + action);
            // Connectivity change
            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
                ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(
                        Context.CONNECTIVITY_SERVICE);
                // TODO Should we use ConnectivityManagerCompat.getNetworkInfoFromBroadcast() instead?
                NetworkInfo info = connectivityManager.getActiveNetworkInfo();
                if (info == null)
                    return;
                String typeName = info.getTypeName();
                String subtypeName = info.getSubtypeName();
                boolean available = info.isAvailable();

                Log.i(TAG, "Network Type: " + typeName + ", subtype: " + subtypeName + ", available: " + available);
                if (info.getType() == ConnectivityManager.TYPE_WIFI) {
                    if (nativeProxyAutoConfigured) {
                        ProxySettings.setConnectionProxy(getApplicationContext(), LOCALHOST, port, "");
                    } else {
                        updateNoTrafficCheck(connectivityManager);
                    }
                }
            }
            // Proxy change
            else if (Proxy.PROXY_CHANGE_ACTION.equals(action)) {
                Object pp = intent.getParcelableExtra("proxy");
                try {
                    String[] userProxy = ProxySettings.getUserProxy(pp);
                    if (nativeProxyAutoConfigured) {
                        if (userProxy != null && Integer.valueOf(userProxy[1]) != port) {
                            Log.i(TAG, "User has set new proxy: " + userProxy[0] + ":" + userProxy[1] + "("
                                    + userProxy[2] + ")");
                            if (proxy != null) {
                                configureUserProxy(proxyConfiguration, userProxy[0], userProxy[1], userProxy[2],
                                        null, null);
                                proxy.restart(proxyConfiguration.getProperty("handler"));
                            }
                        }
                    } else {
                        Log.i(TAG, "User has set proxy: " + userProxy[0] + ":" + userProxy[1] + "(" + userProxy[2]
                                + ")");
                        updateNoTrafficCheck(userProxy);
                    }
                } catch (Exception e) {
                    // This should not happen
                    Log.e(TAG, null, e);
                }
            }
        }
    };

    final class ProxyServer extends Server {
        @Override
        public void close() {
            try {
                listen.close();
                this.interrupt();
                this.join();
            } catch (Exception e) {
                // ignore - it always happens
            }
            log(LOG_WARNING, null, "server stopped");
        }

        @Override
        public void log(int level, Object obj, String message) {
            if (level <= logLevel) {
                Log.println(7 - level, obj != null ? obj.toString() : TAG, message);
            }
        }
    }
}