/*
 * Copyright (c) 2011-2019, Zingaya, Inc. All rights reserved.
 */

package com.reactsocks.vpnforegroundservice;

import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.net.VpnService;
import android.os.ParcelFileDescriptor;

import android.support.v4.content.LocalBroadcastManager;


import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.BroadcastReceiver;
import android.content.IntentFilter;

import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.VpnService;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;

import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;

import java.io.IOException;
import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static com.reactsocks.vpnforegroundservice.Constants.NOTIFICATION_CONFIG;
import static com.reactsocks.vpnforegroundservice.Constants.SERVICE_STOP;


    public class VPNForegroundService extends VpnService {

        private static final String TAG = "VPNForegroundService";


        //private State state = State.none;
        private boolean user_foreground = true;
        private boolean last_connected = false;

        private static Object jni_lock = new Object();
        private static long jni_context = 0;
        private Thread tunnelThread = null;
        private VPNForegroundService.Builder last_builder = null;
        private ParcelFileDescriptor vpn = null;
        private boolean temporarilyStopped = false;

        //private Map<String, Boolean> mapHostsBlocked = new HashMap<>();
        private Map<Integer, Boolean> mapUidAllowed = new HashMap<>();
        private Map<Integer, Integer> mapUidKnown = new HashMap<>();
        //private final Map<IPKey, Map<InetAddress, IPRule>> mapUidIPFilters = new HashMap<>();
        private Map<Integer, Forward> mapForward = new HashMap<>();
        //private Map<Integer, Boolean> mapNotify = new HashMap<>();
        private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);


        private native long jni_init(int sdk);

        private native void jni_start(long context, int loglevel);

        private native void jni_run(long context, int tun, boolean fwd53, int rcode);

        private native void jni_stop(long context);

        private native void jni_clear(long context);

        private native int jni_get_mtu();

        //private native int[] jni_get_stats(long context);

        //private static native void jni_pcap(String name, int record_size, int file_size);

        private native void jni_socks5(String addr, int port, String username, String password);

        private native void jni_done(long context);

        private BroadcastReceiver interactiveStateReceiver;

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


        // Services interface
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            // Start a new session by creating a new thread.
            Log.d(TAG,"onstartcommand");
            String action = intent.getAction();
            if (action != null) {
                if (action.equals(Constants.ACTION_FOREGROUND_SERVICE_START)) {
                    if (intent.getExtras() != null && intent.getExtras().containsKey(NOTIFICATION_CONFIG)) {
                        Bundle notificationConfig = intent.getExtras().getBundle(NOTIFICATION_CONFIG);
                        if (notificationConfig != null && notificationConfig.containsKey("id")) {
                            Notification notification = NotificationHelper.getInstance(getApplicationContext())
                                    .buildNotification(getApplicationContext(), notificationConfig);

                            startForeground((int)notificationConfig.getDouble("id"), notification);
                            start(intent.getExtras().getString("ip"),intent.getExtras().getInt("port"),intent.getExtras().getString("user"),intent.getExtras().getString("password"));

                            Log.d(TAG,"onstartcommand start");
                            IntentFilter filter = new IntentFilter();
                            filter.addAction(SERVICE_STOP);
                            getApplicationContext().registerReceiver(interactiveStateReceiver, filter);

                        }
                    }
                } else if (action.equals(Constants.ACTION_FOREGROUND_SERVICE_STOP)) {
                    Log.d(TAG,"onstartcommand stop");
                    stopSelf();
                }
            }


            return START_STICKY;
        }

        private void start(String ip, int port, String user, String password){



            try {

                Log.d(TAG, "Start foreground");

                List<Rule> listRule = Rule.getRules(true, VPNForegroundService.this);
                List<Rule> listAllowed = getAllowedRules(listRule);

                last_builder = getBuilder(listAllowed, listRule);
                Log.d(TAG,"Builder "+last_builder.networkInfo);
                vpn = startVPN(last_builder);
                if (vpn == null)
                    throw new IllegalStateException(getString((R.string.msg_start_failed)));

                startNative(vpn, listAllowed, listRule,ip,port,user,password);

                // Request garbage collection
                System.gc();
            } catch (Throwable ex) {
                Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));

                if (VpnService.prepare(VPNForegroundService.this) == null) {
                    Log.w(TAG, "VPN prepared connected=" + last_connected);

                } else {
                    Log.w(TAG,ex.toString());
                }

            }

        }


        public static List<InetAddress> getDns(Context context) {
            List<InetAddress> listDns = new ArrayList<>();
            List<String> sysDns = Util.getDefaultDNS(context);

            // Get custom DNS servers
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
            boolean ip6 = prefs.getBoolean("ip6", true);
            boolean pdns = Util.isPrivateDns(context);
            // Make sure normal DNS servers are used when private DNS is enabled
            String vpnDns1 = prefs.getString("dns", pdns ? "8.8.8.8" : null);
            String vpnDns2 = prefs.getString("dns2", pdns ? "8.8.4.4" : null);
            Log.i(TAG, "DNS system=" + TextUtils.join(",", sysDns) + " VPN1=" + vpnDns1 + " VPN2=" + vpnDns2);

            if (vpnDns1 != null)
                try {
                    InetAddress dns = InetAddress.getByName(vpnDns1);
                    if (!(dns.isLoopbackAddress() || dns.isAnyLocalAddress()) &&
                            (ip6 || dns instanceof Inet4Address))
                        listDns.add(dns);
                } catch (Throwable ignored) {
                }

            if (vpnDns2 != null)
                try {
                    InetAddress dns = InetAddress.getByName(vpnDns2);
                    if (!(dns.isLoopbackAddress() || dns.isAnyLocalAddress()) &&
                            (ip6 || dns instanceof Inet4Address))
                        listDns.add(dns);
                } catch (Throwable ex) {
                    Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                }

            // Use system DNS servers only when no two custom DNS servers specified
            if (listDns.size() < 2)
                for (String def_dns : sysDns)
                    try {
                        InetAddress ddns = InetAddress.getByName(def_dns);
                        if (!listDns.contains(ddns) &&
                                !(ddns.isLoopbackAddress() || ddns.isAnyLocalAddress()) &&
                                (ip6 || ddns instanceof Inet4Address))
                            listDns.add(ddns);
                    } catch (Throwable ex) {
                        Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                    }

            // Remove local DNS servers when not routing LAN
            boolean lan = prefs.getBoolean("lan", false);
            boolean use_hosts = prefs.getBoolean("filter", true) && prefs.getBoolean("use_hosts", false);
            if (lan && use_hosts) {
                List<InetAddress> listLocal = new ArrayList<>();
                try {
                    Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
                    if (nis != null)
                        while (nis.hasMoreElements()) {
                            NetworkInterface ni = nis.nextElement();
                            if (ni != null && ni.isUp() && !ni.isLoopback()) {
                                List<InterfaceAddress> ias = ni.getInterfaceAddresses();
                                if (ias != null)
                                    for (InterfaceAddress ia : ias) {
                                        InetAddress hostAddress = ia.getAddress();
                                        BigInteger host = new BigInteger(1, hostAddress.getAddress());

                                        int prefix = ia.getNetworkPrefixLength();
                                        BigInteger mask = BigInteger.valueOf(-1).shiftLeft(hostAddress.getAddress().length * 8 - prefix);

                                        for (InetAddress dns : listDns)
                                            if (hostAddress.getAddress().length == dns.getAddress().length) {
                                                BigInteger ip = new BigInteger(1, dns.getAddress());

                                                if (host.and(mask).equals(ip.and(mask))) {
                                                    Log.i(TAG, "Local DNS server host=" + hostAddress + "/" + prefix + " dns=" + dns);
                                                    listLocal.add(dns);
                                                }
                                            }
                                    }
                            }
                        }
                } catch (Throwable ex) {
                    Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                }

                List<InetAddress> listDns4 = new ArrayList<>();
                List<InetAddress> listDns6 = new ArrayList<>();
                try {
                    listDns4.add(InetAddress.getByName("8.8.8.8"));
                    listDns4.add(InetAddress.getByName("8.8.4.4"));
                    if (ip6) {
                        listDns6.add(InetAddress.getByName("2001:4860:4860::8888"));
                        listDns6.add(InetAddress.getByName("2001:4860:4860::8844"));
                    }

                } catch (Throwable ex) {
                    Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                }

                for (InetAddress dns : listLocal) {
                    listDns.remove(dns);
                    if (dns instanceof Inet4Address) {
                        if (listDns4.size() > 0) {
                            listDns.add(listDns4.get(0));
                            listDns4.remove(0);
                        }
                    } else {
                        if (listDns6.size() > 0) {
                            listDns.add(listDns6.get(0));
                            listDns6.remove(0);
                        }
                    }
                }
            }

            return listDns;
        }

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException {
            try {
                Log.d(TAG,"startvpn");
                ParcelFileDescriptor pfd = builder.establish();
                //Log.d(TAG,"startvpn "+pfd.toString());

                // Set underlying network
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
                    Network active = (cm == null ? null : cm.getActiveNetwork());
                    if (active != null) {
                        Log.i(TAG, "Setting underlying network=" + cm.getNetworkInfo(active));
                        setUnderlyingNetworks(new Network[]{active});
                    }
                }

                return pfd;
            } catch (SecurityException ex) {
                Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                throw ex;
            } catch (Throwable ex) {
                Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                return null;
            }
        }

        private Builder getBuilder(List<Rule> listAllowed, List<Rule> listRule) {
            //SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
            boolean subnet = false;
            boolean tethering = false;
            boolean lan = false;
            boolean ip6 = true;
            boolean filter = true;
            boolean system = false;

            // Build VPN service
            Builder builder = new Builder();
            builder.setSession(getString(R.string.app_name));

            // VPN address
            String vpn4 = "10.1.10.1";
            Log.i(TAG, "vpn4=" + vpn4);
            builder.addAddress(vpn4, 32);
            if (ip6) {
                String vpn6 = "fd00:1:fd00:1:fd00:1:fd00:1";
                Log.i(TAG, "vpn6=" + vpn6);
                builder.addAddress(vpn6, 128);
            }

            // DNS address
            if (filter)
                for (InetAddress dns : getDns(VPNForegroundService.this)) {
                    if (ip6 || dns instanceof Inet4Address) {
                        Log.i(TAG, "dns=" + dns);
                        builder.addDnsServer(dns);
                    }
                }

            // Subnet routing
            if (subnet) {
                // Exclude IP ranges
                List<IPUtil.CIDR> listExclude = new ArrayList<>();
                listExclude.add(new IPUtil.CIDR("127.0.0.0", 8)); // localhost

                if (tethering) {
                    // USB tethering 192.168.42.x
                    // Wi-Fi tethering 192.168.43.x
                    listExclude.add(new IPUtil.CIDR("192.168.42.0", 23));
                    // Bluetooth tethering 192.168.44.x
                    listExclude.add(new IPUtil.CIDR("192.168.44.0", 24));
                    // Wi-Fi direct 192.168.49.x
                    listExclude.add(new IPUtil.CIDR("192.168.49.0", 24));
                }

                if (lan) {
                    try {
                        Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
                        while (nis.hasMoreElements()) {
                            NetworkInterface ni = nis.nextElement();
                            if (ni != null && ni.isUp() && !ni.isLoopback() &&
                                    ni.getName() != null && !ni.getName().startsWith("tun"))
                                for (InterfaceAddress ia : ni.getInterfaceAddresses())
                                    if (ia.getAddress() instanceof Inet4Address) {
                                        IPUtil.CIDR local = new IPUtil.CIDR(ia.getAddress(), ia.getNetworkPrefixLength());
                                        Log.i(TAG, "Excluding " + ni.getName() + " " + local);
                                        listExclude.add(local);
                                    }
                        }
                    } catch (SocketException ex) {
                        Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                    }
                }

                // https://en.wikipedia.org/wiki/Mobile_country_code
                Configuration config = getResources().getConfiguration();

                // T-Mobile Wi-Fi calling
                if (config.mcc == 310 && (config.mnc == 160 ||
                        config.mnc == 200 ||
                        config.mnc == 210 ||
                        config.mnc == 220 ||
                        config.mnc == 230 ||
                        config.mnc == 240 ||
                        config.mnc == 250 ||
                        config.mnc == 260 ||
                        config.mnc == 270 ||
                        config.mnc == 310 ||
                        config.mnc == 490 ||
                        config.mnc == 660 ||
                        config.mnc == 800)) {
                    listExclude.add(new IPUtil.CIDR("66.94.2.0", 24));
                    listExclude.add(new IPUtil.CIDR("66.94.6.0", 23));
                    listExclude.add(new IPUtil.CIDR("66.94.8.0", 22));
                    listExclude.add(new IPUtil.CIDR("208.54.0.0", 16));
                }

                // Verizon wireless calling
                if ((config.mcc == 310 &&
                        (config.mnc == 4 ||
                                config.mnc == 5 ||
                                config.mnc == 6 ||
                                config.mnc == 10 ||
                                config.mnc == 12 ||
                                config.mnc == 13 ||
                                config.mnc == 350 ||
                                config.mnc == 590 ||
                                config.mnc == 820 ||
                                config.mnc == 890 ||
                                config.mnc == 910)) ||
                        (config.mcc == 311 && (config.mnc == 12 ||
                                config.mnc == 110 ||
                                (config.mnc >= 270 && config.mnc <= 289) ||
                                config.mnc == 390 ||
                                (config.mnc >= 480 && config.mnc <= 489) ||
                                config.mnc == 590)) ||
                        (config.mcc == 312 && (config.mnc == 770))) {
                    listExclude.add(new IPUtil.CIDR("66.174.0.0", 16)); // 66.174.0.0 - 66.174.255.255
                    listExclude.add(new IPUtil.CIDR("66.82.0.0", 15)); // 69.82.0.0 - 69.83.255.255
                    listExclude.add(new IPUtil.CIDR("69.96.0.0", 13)); // 69.96.0.0 - 69.103.255.255
                    listExclude.add(new IPUtil.CIDR("70.192.0.0", 11)); // 70.192.0.0 - 70.223.255.255
                    listExclude.add(new IPUtil.CIDR("97.128.0.0", 9)); // 97.128.0.0 - 97.255.255.255
                    listExclude.add(new IPUtil.CIDR("174.192.0.0", 9)); // 174.192.0.0 - 174.255.255.255
                    listExclude.add(new IPUtil.CIDR("72.96.0.0", 9)); // 72.96.0.0 - 72.127.255.255
                    listExclude.add(new IPUtil.CIDR("75.192.0.0", 9)); // 75.192.0.0 - 75.255.255.255
                    listExclude.add(new IPUtil.CIDR("97.0.0.0", 10)); // 97.0.0.0 - 97.63.255.255
                }

                // Broadcast
                listExclude.add(new IPUtil.CIDR("224.0.0.0", 3));

                Collections.sort(listExclude);

                try {
                    InetAddress start = InetAddress.getByName("0.0.0.0");
                    for (IPUtil.CIDR exclude : listExclude) {
                        Log.i(TAG, "Exclude " + exclude.getStart().getHostAddress() + "..." + exclude.getEnd().getHostAddress());
                        for (IPUtil.CIDR include : IPUtil.toCIDR(start, IPUtil.minus1(exclude.getStart())))
                            try {
                                builder.addRoute(include.address, include.prefix);
                            } catch (Throwable ex) {
                                Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                            }
                        start = IPUtil.plus1(exclude.getEnd());
                    }
                    String end = (lan ? "255.255.255.254" : "255.255.255.255");
                    for (IPUtil.CIDR include : IPUtil.toCIDR("224.0.0.0", end))
                        try {
                            builder.addRoute(include.address, include.prefix);
                        } catch (Throwable ex) {
                            Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                        }
                } catch (UnknownHostException ex) {
                    Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                }
            } else
                builder.addRoute("0.0.0.0", 0);

            Log.i(TAG, "IPv6=" + ip6);
            if (ip6)
                builder.addRoute("2000::", 3); // unicast

            // MTU
            int mtu = jni_get_mtu();
            Log.i(TAG, "MTU=" + mtu);
            builder.setMtu(mtu);

            // Add list of allowed applications
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                try {
                    builder.addDisallowedApplication(getPackageName());
                } catch (PackageManager.NameNotFoundException ex) {
                    Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                }
                if (last_connected && !filter)
                    for (Rule rule : listAllowed)
                        try {
                            builder.addDisallowedApplication(rule.packageName);
                        } catch (PackageManager.NameNotFoundException ex) {
                            Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                        }
                else if (filter)
                    for (Rule rule : listRule)
                        if (!rule.apply || (!system && rule.system))
                            try {
                                Log.i(TAG, "Not routing " + rule.packageName);
                                builder.addDisallowedApplication(rule.packageName);
                            } catch (PackageManager.NameNotFoundException ex) {
                                Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                            }
            }

            // Build configure intent
            //Intent configure = new Intent(this, VPNForegroundServiceModule.class);
            //PendingIntent pi = PendingIntent.getActivity(this, 0, configure, PendingIntent.FLAG_UPDATE_CURRENT);
            //builder.setConfigureIntent(pi);

            return builder;
        }

        private void startNative(final ParcelFileDescriptor vpn, List<Rule> listAllowed, List<Rule> listRule,String ip,int port, String user, String password) {

            Log.i(TAG, "Start native");

            // Prepare rules
            prepareUidAllowed(listAllowed, listRule);
            //prepareHostsBlocked();
            //prepareUidIPFilters(null);
            //prepareForwarding();


        /*lock.writeLock().lock();
        mapNotify.clear();
        lock.writeLock().unlock();*/


            int prio = Log.WARN;
            final int rcode = 3;
            //if (prefs.getBoolean("socks5_enabled", false))
            jni_socks5(
                    ip,
                    port,
                    user,
                    password);
            //else
            //    jni_socks5("", 0, "", "");

            if (tunnelThread == null) {
                Log.i(TAG, "Starting tunnel thread");
                jni_start(jni_context, prio);

                tunnelThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Log.i(TAG, "Running tunnel");
                        jni_run(jni_context, vpn.getFd(), mapForward.containsKey(53), rcode);
                        Log.i(TAG, "Tunnel exited");
                        tunnelThread = null;
                    }
                });
                //tunnelThread.setPriority(Thread.MAX_PRIORITY);
                tunnelThread.start();

                Log.i(TAG, "Started tunnel thread");
            }

        }

        private void stopNative(ParcelFileDescriptor vpn, boolean clear) {
            Log.i(TAG, "Stop native clear=" + clear);

            if (tunnelThread != null) {
                Log.i(TAG, "Stopping tunnel thread");

                jni_stop(jni_context);

                Thread thread = tunnelThread;
                while (thread != null)
                    try {
                        thread.join();
                        break;
                    } catch (InterruptedException ignored) {
                    }
                tunnelThread = null;

                if (clear)
                    jni_clear(jni_context);

                Log.i(TAG, "Stopped tunnel thread");
            }
        }

        private void unprepare() {
            lock.writeLock().lock();
            mapUidAllowed.clear();
            mapUidKnown.clear();
        /*mapHostsBlocked.clear();
        mapUidIPFilters.clear();*/
            mapForward.clear();
            //mapNotify.clear();
            lock.writeLock().unlock();
        }

        private void prepareUidAllowed(List<Rule> listAllowed, List<Rule> listRule) {
            lock.writeLock().lock();

            mapUidAllowed.clear();
            for (Rule rule : listAllowed)
                mapUidAllowed.put(rule.uid, true);

            mapUidKnown.clear();
            for (Rule rule : listRule)
                mapUidKnown.put(rule.uid, rule.uid);

            lock.writeLock().unlock();
        }



        private List<Rule> getAllowedRules(List<Rule> listRule) {
            List<Rule> listAllowed = new ArrayList<>();

            for (Rule rule : listRule) {
                listAllowed.add(rule);
            }

            Log.i(TAG, "Allowed " + listAllowed.size() + " of " + listRule.size());
            return listAllowed;
        }

        private void stopVPN(ParcelFileDescriptor pfd) {
            Log.i(TAG, "Stopping");
            try {
                pfd.close();
            } catch (IOException ex) {
                Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
            }
        }

        // Called from native code
        private void nativeExit(String reason) {
            Log.w(TAG, "Native exit reason=" + reason);
            if (reason != null) {
                //showErrorNotification(reason);
                Log.w(TAG,reason);
                SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
                prefs.edit().putBoolean("enabled", false).apply();
                //WidgetMain.updateWidgets(this);
            }
        }

        // Called from native code
        private void nativeError(int error, String message) {
            Log.w(TAG, "Native error " + error + ": " + message);
            //showErrorNotification(message);
        }

        // Called from native code
        private void logPacket(Packet packet) {
        /*Message msg = logHandler.obtainMessage();
        msg.obj = packet;
        msg.what = MSG_PACKET;
        msg.arg1 = (last_connected ? (last_metered ? 2 : 1) : 0);
        msg.arg2 = (last_interactive ? 1 : 0);
        logHandler.sendMessage(msg);*/
            Log.d(TAG,"Log packet "+packet);
        }

        // Called from native code
        private void dnsResolved(ResourceRecord rr) {
            if (DatabaseHelper.getInstance(VPNForegroundService.this).insertDns(rr)) {
                Log.i(TAG, "New IP " + rr);
                //prepareUidIPFilters(rr.QName);
            }
        }

        // Called from native code
        private boolean isDomainBlocked(String name) {
        /*lock.readLock().lock();
        boolean blocked = (mapHostsBlocked.containsKey(name) && mapHostsBlocked.get(name));
        lock.readLock().unlock();
        return blocked;*/
            return false;
        }

        // Called from native code
        @TargetApi(Build.VERSION_CODES.Q)
        private int getUidQ(int version, int protocol, String saddr, int sport, String daddr, int dport) {
            if (protocol != 6 /* TCP */ && protocol != 17 /* UDP */)
                return Process.INVALID_UID;

            ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
            if (cm == null)
                return Process.INVALID_UID;

            InetSocketAddress local = new InetSocketAddress(saddr, sport);
            InetSocketAddress remote = new InetSocketAddress(daddr, dport);

            Log.i(TAG, "Get uid local=" + local + " remote=" + remote);
            int uid = cm.getConnectionOwnerUid(protocol, local, remote);
            Log.i(TAG, "Get uid=" + uid);
            return uid;
        }

        private boolean isSupported(int protocol) {
            return (protocol == 1 /* ICMPv4 */ ||
                    protocol == 58 /* ICMPv6 */ ||
                    protocol == 6 /* TCP */ ||
                    protocol == 17 /* UDP */);
        }

        // Called from native code
        private Allowed isAddressAllowed(Packet packet) {
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

            lock.readLock().lock();

            packet.allowed = false;
            if (prefs.getBoolean("filter", true)) {
                // https://android.googlesource.com/platform/system/core/+/master/include/private/android_filesystem_config.h
                if (packet.protocol == 17 /* UDP */ && !prefs.getBoolean("filter_udp", false)) {
                    // Allow unfiltered UDP
                    packet.allowed = true;
                    Log.i(TAG, "Allowing UDP " + packet);
                } else if (packet.uid < 2000 &&
                        !last_connected && isSupported(packet.protocol)) {
                    // Allow system applications in disconnected state
                    packet.allowed = true;
                    Log.w(TAG, "Allowing disconnected system " + packet);
                } else if (packet.uid < 2000 &&
                        !mapUidKnown.containsKey(packet.uid) && isSupported(packet.protocol)) {
                    // Allow unknown system traffic
                    packet.allowed = true;
                    Log.w(TAG, "Allowing unknown system " + packet);
                } else if (packet.uid == Process.myUid()) {
                    // Allow self
                    packet.allowed = true;
                    Log.w(TAG, "Allowing self " + packet);
                } else {
                    boolean filtered = false;
                    IPKey key = new IPKey(packet.version, packet.protocol, packet.dport, packet.uid);
                /*if (mapUidIPFilters.containsKey(key))
                    try {
                        InetAddress iaddr = InetAddress.getByName(packet.daddr);
                        Map<InetAddress, IPRule> map = mapUidIPFilters.get(key);
                        if (map != null && map.containsKey(iaddr)) {
                            IPRule rule = map.get(iaddr);
                            if (rule.isExpired())
                                Log.i(TAG, "DNS expired " + packet + " rule " + rule);
                            else {
                                filtered = true;
                                packet.allowed = !rule.isBlocked();
                                Log.i(TAG, "Filtering " + packet +
                                        " allowed=" + packet.allowed + " rule " + rule);
                            }
                        }
                    } catch (UnknownHostException ex) {
                        Log.w(TAG, "Allowed " + ex.toString() + "\n" + Log.getStackTraceString(ex));
                    }*/

                    if (!filtered)
                        if (mapUidAllowed.containsKey(packet.uid))
                            packet.allowed = mapUidAllowed.get(packet.uid);
                        else
                            Log.w(TAG, "No rules for " + packet);
                }
            }

            Allowed allowed = null;
            if (packet.allowed) {
                if (mapForward.containsKey(packet.dport)) {
                    Forward fwd = mapForward.get(packet.dport);
                    if (fwd.ruid == packet.uid) {
                        allowed = new Allowed();
                    } else {
                        allowed = new Allowed(fwd.raddr, fwd.rport);
                        packet.data = "> " + fwd.raddr + "/" + fwd.rport;
                    }
                } else
                    allowed = new Allowed();
            }

            lock.readLock().unlock();

            if (prefs.getBoolean("log", false) || prefs.getBoolean("log_app", false))
                if (packet.protocol != 6 /* TCP */ || !"".equals(packet.flags))
                    if (packet.uid != Process.myUid())
                        Log.d(TAG,"log");
            //logPacket(packet);

            return allowed;
        }

        // Called from native code
        private void accountUsage(Usage usage) {
        /*Message msg = logHandler.obtainMessage();
        msg.obj = usage;
        msg.what = MSG_USAGE;
        logHandler.sendMessage(msg);*/
            Log.d(TAG,"Log usage "+usage);
        }


        @Override
        public void onCreate() {
            Log.i(TAG, "Create version=" + Util.getSelfVersionName(this) + "/" + Util.getSelfVersionCode(this));

            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

            interactiveStateReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    Log.d(TAG,"On receive broascast "+intent);
                    switch (intent.getAction()) {
                        case SERVICE_STOP:
                            Log.d(TAG,"On receive broascast "+intent);
                            try {
                                if (vpn != null) {
                                    stopNative(vpn, true);
                                    stopVPN(vpn);
                                    vpn = null;
                                    unprepare();
                                }
                            } catch (Throwable ex) {
                                Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
                            }

                            synchronized (jni_lock) {
                                jni_done(jni_context);
                                jni_context = 0;
                            }
                            stopSelf();
                            break;
                    }

                }
            };

            if (jni_context != 0) {
                jni_stop(jni_context);
                synchronized (jni_lock) {
                    jni_done(jni_context);
                    jni_context = 0;
                }
            }

            // Native init
            jni_context = jni_init(Build.VERSION.SDK_INT);
            //boolean pcap = prefs.getBoolean("pcap", false);
            //setPcap(pcap, this);

            //prefs.registerOnSharedPreferenceChangeListener(this);

            //Util.setTheme(this);
            super.onCreate();


        }




        @Override
        public void onDestroy() {
            Log.i(TAG, "Destroy");
            getApplicationContext().unregisterReceiver(interactiveStateReceiver);
            super.onDestroy();
        }




        private class Builder extends VpnService.Builder {
            private NetworkInfo networkInfo;
            private int mtu;
            private List<String> listAddress = new ArrayList<>();
            private List<String> listRoute = new ArrayList<>();
            private List<InetAddress> listDns = new ArrayList<>();
            private List<String> listDisallowed = new ArrayList<>();

            private Builder() {
                super();
                ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
                networkInfo = cm.getActiveNetworkInfo();
            }

            @Override
            public VpnService.Builder setMtu(int mtu) {
                this.mtu = mtu;
                super.setMtu(mtu);
                return this;
            }

            @Override
            public Builder addAddress(String address, int prefixLength) {
                listAddress.add(address + "/" + prefixLength);
                super.addAddress(address, prefixLength);
                return this;
            }

            @Override
            public Builder addRoute(String address, int prefixLength) {
                listRoute.add(address + "/" + prefixLength);
                super.addRoute(address, prefixLength);
                return this;
            }

            @Override
            public Builder addRoute(InetAddress address, int prefixLength) {
                listRoute.add(address.getHostAddress() + "/" + prefixLength);
                super.addRoute(address, prefixLength);
                return this;
            }

            @Override
            public Builder addDnsServer(InetAddress address) {
                listDns.add(address);
                super.addDnsServer(address);
                return this;
            }

            @Override
            public Builder addDisallowedApplication(String packageName) throws PackageManager.NameNotFoundException {
                listDisallowed.add(packageName);
                super.addDisallowedApplication(packageName);
                return this;
            }

            @Override
            public boolean equals(Object obj) {
                Builder other = (Builder) obj;

                if (other == null)
                    return false;

                if (this.networkInfo == null || other.networkInfo == null ||
                        this.networkInfo.getType() != other.networkInfo.getType())
                    return false;

                if (this.mtu != other.mtu)
                    return false;

                if (this.listAddress.size() != other.listAddress.size())
                    return false;

                if (this.listRoute.size() != other.listRoute.size())
                    return false;

                if (this.listDns.size() != other.listDns.size())
                    return false;

                if (this.listDisallowed.size() != other.listDisallowed.size())
                    return false;

                for (String address : this.listAddress)
                    if (!other.listAddress.contains(address))
                        return false;

                for (String route : this.listRoute)
                    if (!other.listRoute.contains(route))
                        return false;

                for (InetAddress dns : this.listDns)
                    if (!other.listDns.contains(dns))
                        return false;

                for (String pkg : this.listDisallowed)
                    if (!other.listDisallowed.contains(pkg))
                        return false;

                return true;
            }
        }

        private class IPKey {
            int version;
            int protocol;
            int dport;
            int uid;

            public IPKey(int version, int protocol, int dport, int uid) {
                this.version = version;
                this.protocol = protocol;
                // Only TCP (6) and UDP (17) have port numbers
                this.dport = (protocol == 6 || protocol == 17 ? dport : 0);
                this.uid = uid;
            }

            @Override
            public boolean equals(Object obj) {
                if (!(obj instanceof IPKey))
                    return false;
                IPKey other = (IPKey) obj;
                return (this.version == other.version &&
                        this.protocol == other.protocol &&
                        this.dport == other.dport &&
                        this.uid == other.uid);
            }

            @Override
            public int hashCode() {
                return (version << 40) | (protocol << 32) | (dport << 16) | uid;
            }

            @Override
            public String toString() {
                return "v" + version + " p" + protocol + " port=" + dport + " uid=" + uid;
            }
        }

        private class IPRule {
            private IPKey key;
            private String name;
            private boolean block;
            private long expires;

            public IPRule(IPKey key, String name, boolean block, long expires) {
                this.key = key;
                this.name = name;
                this.block = block;
                this.expires = expires;
            }

            public boolean isBlocked() {
                return this.block;
            }

            public boolean isExpired() {
                return System.currentTimeMillis() > this.expires;
            }

            public void updateExpires(long expires) {
                this.expires = Math.max(this.expires, expires);
            }

            @Override
            public boolean equals(Object obj) {
                IPRule other = (IPRule) obj;
                return (this.block == other.block && this.expires == other.expires);
            }

            @Override
            public String toString() {
                return this.key + " " + this.name;
            }
        }

    }