public class FwdApplication extends MultiDexApplication {

    private static final String TAG = "FwdApplication";
    private Tracker mTracker;

    /**
     * Gets the default {@link Tracker} for this {@link Application}.
     *
     * @return tracker
     */
    synchronized public Tracker getDefaultTracker() {
        if (mTracker == null) {
            GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
            // To enable debug logging use: adb shell setprop log.tag.GAv4 DEBUG

            if (BuildConfig.DEBUG) {
                mTracker = analytics.newTracker(R.xml.analytics_debug);
            } else {
                mTracker = analytics.newTracker(R.xml.analytics);
            }
            mTracker.enableAutoActivityTracking(true);
        }
        return mTracker;
    }


} public class RuleDao {

    private SQLiteDatabase db;
    private RuleDbHelper ruleDbHelper;

    public RuleDao(RuleDbHelper ruleDbHelper) {
        this.ruleDbHelper = ruleDbHelper;
    }

    public RuleDao(SQLiteDatabase sqLiteDatabase, RuleDbHelper ruleDbHelper) {
        this.db = sqLiteDatabase;
        this.ruleDbHelper = ruleDbHelper;
    }

    /**
     * Inserts a valid rule into the SQLite database.
     *
     * @param ruleModel The source {@link RuleModel}.
     * @return the id of the inserted rule.
     */
    public long insertRule(RuleModel ruleModel) {
        // Gets the data repository in write mode
        this.db = ruleDbHelper.getWritableDatabase();

        ContentValues constantValues = RuleHelper.ruleModelToContentValues(ruleModel);

        long newRowId = db.insert(
                RuleContract.RuleEntry.TABLE_NAME,
                null,
                constantValues);

        return newRowId;
    }

    /**
     * Finds and returns a list of all rules.
     *
     * @return a list of all {@link RuleModel} objects.
     */
    public List<RuleModel> getAllRuleModels() {

        List<RuleModel> ruleModels = new LinkedList<RuleModel>();

        // Gets the data repository in read mode
        this.db = ruleDbHelper.getReadableDatabase();

        // Define a projection that specifies which columns from the database
        // you will actually use after this query.
        String[] projection = RuleDbHelper.generateAllRowsSelection();

        // How you want the results sorted in the resulting Cursor
        String sortOrder =
                RuleContract.RuleEntry.COLUMN_NAME_RULE_ID + " DESC";

        Cursor cursor = db.query(
                RuleContract.RuleEntry.TABLE_NAME,          // The table to query
                projection,                                 // The columns to return
                null,                                       // The columns for the WHERE clause
                null,                                       // The values for the WHERE clause
                null,                                       // don't group the rows
                null,                                       // don't filter by row groups
                sortOrder                                   // The sort order
        );

        cursor.moveToFirst();
        while (!cursor.isAfterLast()) {
            RuleModel ruleModel = RuleHelper.cursorToRuleModel(cursor);
            ruleModels.add(ruleModel);
            cursor.moveToNext();
        }
        // make sure to close the cursor
        cursor.close();

        return ruleModels;
    }

    public List<RuleModel> getAllEnabledRuleModels() {
        List<RuleModel> ruleModels = new LinkedList<RuleModel>();
        List<RuleModel> enabledRuleModels = new LinkedList<RuleModel>();

        ruleModels = getAllRuleModels();

        for (RuleModel ruleModel : ruleModels) {
            if (ruleModel.isEnabled()) {
                enabledRuleModels.add(ruleModel);
            }
        }

        return enabledRuleModels;
    }
} public class RuleDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 3;
    public static final String DATABASE_NAME = "Rule.db";

    private static final String TEXT_TYPE = " TEXT";
    private static final String INTEGER_TYPE = " INTEGER";
    private static final String COMMA_SEP = ",";
    private static final String SQL_CREATE_ENTRIES =
            "CREATE TABLE " + RuleContract.RuleEntry.TABLE_NAME + " (" +
                    RuleContract.RuleEntry.COLUMN_NAME_RULE_ID + " INTEGER PRIMARY KEY," +
                    RuleContract.RuleEntry.COLUMN_NAME_NAME + TEXT_TYPE + COMMA_SEP +
                    RuleContract.RuleEntry.COLUMN_NAME_IS_TCP + INTEGER_TYPE + COMMA_SEP +
                    RuleContract.RuleEntry.COLUMN_NAME_IS_UDP + INTEGER_TYPE + COMMA_SEP +
                    RuleContract.RuleEntry.COLUMN_NAME_FROM_INTERFACE_NAME + TEXT_TYPE + COMMA_SEP +
                    RuleContract.RuleEntry.COLUMN_NAME_FROM_PORT + INTEGER_TYPE + COMMA_SEP +
                    RuleContract.RuleEntry.COLUMN_NAME_TARGET_IP_ADDRESS + TEXT_TYPE + COMMA_SEP +
                    RuleContract.RuleEntry.COLUMN_NAME_TARGET_PORT + INTEGER_TYPE + COMMA_SEP +
                    RuleContract.RuleEntry.COLUMN_NAME_IS_ENABLED + INTEGER_TYPE +
                    " )";

    private static final String SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS " + RuleContract.RuleEntry.TABLE_NAME;

    private static final String DATABASE_ALTER_RULES_1 = String.format("ALTER TABLE %s ADD COLUMN %s int default 1;",
            RuleContract.RuleEntry.TABLE_NAME, RuleContract.RuleEntry.COLUMN_NAME_IS_ENABLED);

    public RuleDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_ENTRIES);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        if (oldVersion < 3) {
            db.execSQL(DATABASE_ALTER_RULES_1);
        }

    }

    public static String[] generateAllRowsSelection() {
        String[] projection = {
                RuleContract.RuleEntry.COLUMN_NAME_RULE_ID,
                RuleContract.RuleEntry.COLUMN_NAME_NAME,
                RuleContract.RuleEntry.COLUMN_NAME_IS_TCP,
                RuleContract.RuleEntry.COLUMN_NAME_IS_UDP,
                RuleContract.RuleEntry.COLUMN_NAME_FROM_INTERFACE_NAME,
                RuleContract.RuleEntry.COLUMN_NAME_FROM_PORT + TEXT_TYPE,
                RuleContract.RuleEntry.COLUMN_NAME_TARGET_IP_ADDRESS,
                RuleContract.RuleEntry.COLUMN_NAME_TARGET_PORT,
                RuleContract.RuleEntry.COLUMN_NAME_IS_ENABLED
        };

        return projection;
    }
} public class ForwardingManager implements Serializable {

    private static ForwardingManager instance = null;


    private ForwardingManager() {

    }

    /**
     * Return an instance of the {@link ForwardingManager} class.
     *
     * @return
     */
    public static ForwardingManager getInstance() {
        if (instance == null) {
            // Thread Safe. Might be costly operation in some case
            synchronized (ForwardingManager.class) {
                if (instance == null) {
                    instance = new ForwardingManager();
                }
            }
        }
        return instance;
    }

    private boolean isEnabled = false;

    public boolean isEnabled() {
        return isEnabled;
    }

    protected void enableForwarding() {
        this.isEnabled = true;
    }

    protected void disableForwarding() {
        this.isEnabled = false;
    }


} public class ForwardingService extends IntentService {

    // Defines a custom Intent action
    public static final String BROADCAST_ACTION =
            "com.elixsr.portforwarder.forwarding.ForwardingService.BROADCAST";

    // Defines the key for the status "extra" in an Intent
    public static final String EXTENDED_DATA_STATUS =
            "com.elixsr.portforwarder.forwarding.ForwardingService.STATUS";

    public static final String PORT_FORWARD_SERVICE_STATE =
            "com.elixsr.portforwarder.forwarding.ForwardingService.PORT_FORWARD_STATE";

    public static final String PORT_FORWARD_SERVICE_ERROR_MESSAGE =
            "com.elixsr.portforwarder.forwarding.ForwardingService.PORT_FORWARD_ERROR_MESSAGE";

    private static final String PORT_FORWARD_SERVICE_WAKE_LOCK_TAG = "PortForwardServiceWakeLockTag";

    private static final String TAG = "ForwardingService";

    private static final String CATEGORY_FORWARDING = "Forwarding";

    private static final int NOTIFICATION_ID = 1;
    private static final String ACTION_START_FORWARDING = "Start - Java NIO";
    private static final String LABEL_FORWARDING_TYPE = "";
    private static final String ACTION_STOP_FORWARDING = "Stop - Java NIO";

    private String status = "Test";

    private boolean runService = false;

    //change the magic number
    private ExecutorService executorService;

    //wake lock
    private PowerManager.WakeLock wakeLock;
    private Tracker tracker;

    /**
     * Default constructor for {@link ForwardingService}.#
     * <p>
     * Creates a new instance of ForwardingService and initialises an {@link ExecutorService}
     * with a fixed thread pool of 30 threads.
     */
    public ForwardingService() {
        super(TAG);
        executorService = Executors.newFixedThreadPool(30);
    }

    public ForwardingService(ExecutorService executorService) {
        super(TAG);
        this.executorService = executorService;
    }

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

        /*
        Sourced from: https://developer.android.com/intl/ja/training/scheduling/wakelock.html
         */
        PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                PORT_FORWARD_SERVICE_WAKE_LOCK_TAG);
        wakeLock.acquire();

        tracker = ((FwdApplication) this.getApplication()).getDefaultTracker();
    }

    /**
     * Starts forwarding based on rules found in database.
     * <p>
     * Acquires an instance of the Forwarding Manager to turn forwarding flag on.
     * <p>
     * Creates a list off callbacks for each forward thread, and handle exceptions as they come.
     * <p>
     * If an exception is thrown, the service immediately stops, and the #onDestroy method is
     * called.
     *
     * @param intent
     */
    @Override
    protected void onHandleIntent(Intent intent) {

        // Gets data from the incoming Intent
//        String dataString = intent.getDataString();

        Log.i(TAG, "Ran the service");

        ForwardingManager.getInstance().enableForwarding();


        runService = true;

        /*
         * Creates a new Intent containing a Uri object
         * BROADCAST_ACTION is a custom Intent action
         */
        Intent localIntent =
                new Intent(BROADCAST_ACTION)
                        // Puts the status into the Intent
                        .putExtra(PORT_FORWARD_SERVICE_STATE, ForwardingManager.getInstance().isEnabled());
        // Broadcasts the Intent to receivers in this app.
        LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);

        showForwardingEnabledNotification();

        //load the rules from the datastore
        //TODO: inject the rules as extras
        RuleDao ruleDao = new RuleDao(new RuleDbHelper(this));
        List<RuleModel> ruleModels = ruleDao.getAllEnabledRuleModels();

        List<Forwarder> ruleModelForwarders = new ArrayList<>();

        InetSocketAddress from;

        Forwarder forwarder = null;

        // how many futures there are to check
        int remainingFutures = 0;

        for (RuleModel ruleModel : ruleModels) {

            // Something has killed the runService, no point in looping anymore
            if (!runService) {
                break;
            }

            try {
                from = generateFromIpUsingInterface(ruleModel.getFromInterfaceName(), ruleModel.getFromPort());

                if (ruleModel.isTcp() && runService) {
                    ruleModelForwarders.add(new TcpForwarder(from, ruleModel.getTarget(), ruleModel.getName()));
                    remainingFutures++;
                }

                if (ruleModel.isUdp() && runService) {
                    ruleModelForwarders.add(new UdpForwarder(from, ruleModel.getTarget(), ruleModel.getName()));
                    remainingFutures++;
                }

            } catch (SocketException | ObjectNotFoundException e) {
                Log.e(TAG, "Error generating IP Address for FROM interface with rule '" + ruleModel.getName() + "'", e);

                // graceful UI Exception handling - broadcast this to ui - it will deal with display something to the user e.g. a Toast
                localIntent =
                        new Intent(BROADCAST_ACTION)
                                // Puts the status into the Intent
                                .putExtra(PORT_FORWARD_SERVICE_ERROR_MESSAGE, getString(R.string.start_rule_error_message) + " '" + ruleModel.getName() + "'");
                // Broadcasts the Intent to receivers in this app.
                LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
            }
        }

        executorService = Executors.newFixedThreadPool(ruleModelForwarders.size());

        /*
         Sourced from: http://stackoverflow.com/questions/19348248/waiting-on-a-list-of-future
         */
        CompletionService<Void> completionService =
                new ExecutorCompletionService<>(executorService);

        for (Forwarder ruleForwarder: ruleModelForwarders) {
            completionService.submit(ruleForwarder);
        }

            // Build and send an Event.
        tracker.send(new HitBuilders.EventBuilder()
                .setCategory(CATEGORY_FORWARDING)
                .setAction(ACTION_START_FORWARDING)
                .setLabel(ruleModels.size() + " rules")
                .build());


        Future<?> completedFuture;

        // loop through each callback, and handle an exception
        while (remainingFutures > 0 && runService) {

            // block until a callable completes
            try {
                completedFuture = completionService.take();
                remainingFutures--;

                completedFuture.get();
            } catch (ExecutionException e) {
                Throwable cause = e.getCause();

                Log.e(TAG, "Error when forwarding port.", e);
                localIntent =
                        new Intent(BROADCAST_ACTION)
                                // Puts the status into the Intent
                                .putExtra(PORT_FORWARD_SERVICE_ERROR_MESSAGE, e.getCause().getMessage());
                // Broadcasts the Intent to receivers in this app.
                LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);

                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private InetSocketAddress generateFromIpUsingInterface(String interfaceName, int port) throws SocketException, ObjectNotFoundException {

        String address = null;
        InetSocketAddress inetSocketAddress;

        for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
            NetworkInterface intf = en.nextElement();

            Log.d(TAG, intf.getDisplayName() + " vs " + interfaceName);
            if (intf.getDisplayName().equals(interfaceName)) {

                Log.i(TAG, "Found the relevant Interface. Will attempt to fetch IP Address");

                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {

                    InetAddress inetAddress = enumIpAddr.nextElement();

                    address = new String(inetAddress.getHostAddress().toString());

                    if (address != null & address.length() > 0 && inetAddress instanceof Inet4Address) {

                        inetSocketAddress = new InetSocketAddress(address, port);
                        return inetSocketAddress;
                    }
                }
            }
        }

        //Failed to find the relevant interface
        //TODO: complete
//        Toast.makeText(this, "Could not find relevant network interface.",
//                Toast.LENGTH_LONG).show();
        throw new ObjectNotFoundException("Could not find IP Address for Interface " + interfaceName);
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        super.onTaskRemoved(rootIntent);
        Log.i(TAG, "onTaskRemoved: called");

        // Build and send an Event.
        tracker.send(new HitBuilders.EventBuilder()
                .setCategory(CATEGORY_FORWARDING)
                .setAction(ACTION_STOP_FORWARDING)
                .setLabel("Task Removed")
                .build());

        this.onDestroy();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        runService = false;

        // Reject any new tasks
        executorService.shutdown();

        try {
            // Shutdown any existing tasks
            executorService.shutdownNow();
            if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
                Log.e(TAG, "onDestroy: Pool did not terminate");
            }
        } catch (InterruptedException ie) {
            // (Re-)Cancel if current thread also interrupted
            executorService.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }

        ForwardingManager.getInstance().disableForwarding();

        hideForwardingEnabledNotification();

        //update the main activity
        Intent localIntent =
                new Intent(BROADCAST_ACTION)
                        // Puts the status into the Intent
                        .putExtra(PORT_FORWARD_SERVICE_STATE, ForwardingManager.getInstance().isEnabled());
        // Broadcasts the Intent to receivers in this app.
        LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);

        wakeLock.release();

        // Build and send an Event.
        tracker.send(new HitBuilders.EventBuilder()
                .setCategory(CATEGORY_FORWARDING)
                .setAction(ACTION_STOP_FORWARDING)
                .setLabel("Ended")
                .build());
        Log.i(TAG, "Ended the ForwardingService. Cleanup finished.");
    }

    private void hideForwardingEnabledNotification() {

        NotificationManager mNotificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        mNotificationManager.cancel(NOTIFICATION_ID);
    }

    /**
     * Construct a notification
     */
    private void showForwardingEnabledNotification() {
        NotificationCompat.Builder mBuilder =
                new NotificationCompat.Builder(this)
                        .setSmallIcon(R.drawable.ic_fwd_24dp)
                        .setContentTitle(getString(R.string.notification_forwarding_active_title))
                        .setContentText(getString(R.string.notification_forwarding_touch_disable_text));

        mBuilder.setColor(ContextCompat.getColor(this, R.color.colorPrimaryDark));

        // Creates an explicit intent for an Activity in your app
        Intent resultIntent = new Intent(this, MainActivity.class);

        // The stack builder object will contain an artificial back stack for the
        // started Activity.
        // This ensures that navigating backward from the Activity leads out of
        // your application to the Home screen.
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);

        // Adds the back stack for the Intent (but not the Intent itself)
        stackBuilder.addParentStack(MainActivity.class);

        // Adds the Intent that starts the Activity to the top of the stack
        stackBuilder.addNextIntent(resultIntent);
        PendingIntent resultPendingIntent =
                stackBuilder.getPendingIntent(
                        0,
                        PendingIntent.FLAG_UPDATE_CURRENT
                );
        mBuilder.setContentIntent(resultPendingIntent);
        NotificationManager mNotificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        Notification notification = mBuilder.build();
        notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT | Notification.DEFAULT_LIGHTS;

        // mId allows you to update the notification later on.
        mNotificationManager.notify(NOTIFICATION_ID, notification);
    }
} public class RuleModel implements Serializable {

    private static final String TAG = "RuleModel";

    @Expose(serialize = false, deserialize = false)
    private long id;

    @Expose
    private boolean isTcp;

    @Expose
    private boolean isUdp;

    @Expose
    private String name;

    //TODO: create a class? - worth the effort?
    private String fromInterfaceName;

    @Expose
    private int fromPort;

    @Expose
    private InetSocketAddress target;

    private boolean isEnabled = true;

    // Null constructor - for object building
    public RuleModel() {

    }

    public RuleModel(boolean isTcp, boolean isUdp, String name, String fromInterfaceName, int fromPort, InetSocketAddress target) {
        this.isTcp = isTcp;
        this.isUdp = isUdp;
        this.name = name;
        this.fromInterfaceName = fromInterfaceName;
        this.fromPort = fromPort;
        this.target = target;
    }

    public RuleModel(boolean isTcp, boolean isUdp, String name, String fromInterfaceName, int fromPort, String targetIp, int targetPort) {
        this(isTcp, isUdp, name, fromInterfaceName, fromPort, new InetSocketAddress(targetIp, targetPort));
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public boolean isTcp() {
        return isTcp;
    }

    public void setIsTcp(boolean isTcp) {
        this.isTcp = isTcp;
    }

    public boolean isUdp() {
        return isUdp;
    }

    public void setIsUdp(boolean isUdp) {
        this.isUdp = isUdp;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getFromInterfaceName() {
        return fromInterfaceName;
    }

    public void setFromInterfaceName(String fromInterfaceName) {
        this.fromInterfaceName = fromInterfaceName;
    }

    public int getFromPort() {
        return fromPort;
    }

    public void setFromPort(int fromPort) {
        this.fromPort = fromPort;
    }

    public InetSocketAddress getTarget() {
        return target;
    }

    public void setTarget(InetSocketAddress target) {
        this.target = target;
    }

    public String protocolToString() {
        return RuleHelper.getRuleProtocolFromModel(this);
    }

    /**
     * Return a string of the target IPv4 address
     *
     * @return the IPv4 address as a String
     */
    public String getTargetIpAddress() {
        return this.target.getAddress().getHostAddress();
    }

    /**
     * Return the target port as an integer
     *
     * @return the target port integer.
     */
    public int getTargetPort() {
        return this.target.getPort();
    }

    /**
     * Validate all data held within the model.
     * <p/>
     * Validation rules: <ul> <li>Name should not be null & greater than 0 characters</li>
     * <li>Either TCP or UDP should be true</li> <li>From Interface should not be null & greater
     * than 0 characters</li> <li>From port should be greater than minimum port and smaller than
     * max</li> <li>Target port should be greater than minimum port and smaller than max </li
     * <li>Target IP address should not be null & greater than 0 characters</li> </ul>
     *
     * @return true if valid, false if not valid.
     */
    public boolean isValid() {

        // Ensure the rule has a name
        if (name == null || name.length() <= 0) {
            return false;
        }

        // It must either be one or the other, or even both
        if (!isTcp && !isUdp) {
            return false;
        }

        if (fromInterfaceName == null || fromInterfaceName.length() <= 0) {
            return false;
        }

        if (fromPort < RuleHelper.MIN_PORT_VALUE || fromPort > RuleHelper.MAX_PORT_VALUE) {
            return false;
        }

        try {
            // Ensure that the value is greater than the minimum, and smaller than max
            if (getTargetPort() <= 0 || getTargetPort() < RuleHelper.TARGET_MIN_PORT || getTargetPort() > RuleHelper.MAX_PORT_VALUE) {
                return false;
            }
        } catch (NullPointerException e) {
            Log.e(TAG, "Target object was null.", e);
            return false;
        }


        // The new rule activity should take care of IP address validation
        if (getTargetIpAddress() == null || name.length() <= 0) {
            return false;
        }

        return true;

    }

    public boolean isEnabled() {
        return isEnabled;
    }

    public void setEnabled(boolean enabled) {
        isEnabled = enabled;
    }
} public class RuleListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final String TAG = "RuleListAdapter";
    private List<RuleModel> ruleModels;
    private List<ListItem> listItems;
    private ForwardingManager forwardingManager;

    protected static final int AD_VIEW = 1;
    protected static final int RULE_VIEW = 0;


    public static class AdViewHolder extends RecyclerView.ViewHolder {

        public static final String DARK_AD_ID = "ca-app-pub-9546697987163387/1828769767";
        public static final String LIGHT_AD_ID = "ca-app-pub-9546697987163387/1828769767";
        public static final String PREF_DARK_THEME = "pref_dark_theme";

        public AdViewHolder(View v) {
            super(v);

            AdRequest request = new AdRequest.Builder()
                    .addTestDevice(AdRequest.DEVICE_ID_EMULATOR)
                    .build();
            AdView adView = new AdView(v.getContext());
            adView.setAdSize(AdSize.SMART_BANNER);

            // Load ad type based on theme - dark or light
            if (PreferenceManager.getDefaultSharedPreferences(v.getContext())
                    .getBoolean(PREF_DARK_THEME, false)) {
                adView.setAdUnitId(DARK_AD_ID);
            } else {
                adView.setAdUnitId(LIGHT_AD_ID);
            }
            ((LinearLayout) v).addView(adView, 1);
            adView.loadAd(request);
        }
    }

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder
    public static class RuleViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        // each data item is just a string in this case
        public TextView ruleProtocolText;
        public TextView ruleNameText;
        public TextView ruleFromPortText;
        public TextView ruleTargetPortText;
        private ForwardingManager forwardingManager;
        private long ruleId;


        public RuleViewHolder(View v, ForwardingManager forwardingManager) {
            super(v);
            this.forwardingManager = forwardingManager;
            this.ruleId = ruleId;
            ruleProtocolText = (TextView) v.findViewById(R.id.rule_item_protocol);
            ruleNameText = (TextView) v.findViewById(R.id.rule_item_name);
            ruleFromPortText = (TextView) v.findViewById(R.id.rule_item_from_port);
            ruleTargetPortText = (TextView) v.findViewById(R.id.rule_item_target_port);

            v.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {

            //ensure that editing cannot happen while tethering is enabled
            if (!forwardingManager.isEnabled()) {
                Intent editRuleIntent = new Intent(view.getContext(), EditRuleActivity.class);
                editRuleIntent.putExtra("RuleModelLocation", getAdapterPosition());
                editRuleIntent.putExtra("RuleModelId", this.ruleId);
                view.getContext().startActivity(editRuleIntent);
            }
        }
    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public RuleListAdapter(List<RuleModel> ruleModels, ForwardingManager forwardingManager, Context context) {
        this.ruleModels = ruleModels;
        this.forwardingManager = forwardingManager;
        this.listItems = new ArrayList<>();

        for (RuleModel rule : ruleModels) {
            ListItem<RuleModel> ruleListItem = new ListItem<>(RULE_VIEW);
            ruleListItem.setPayload(rule);
            this.listItems.add(ruleListItem);
        }

        if (!(PreferenceManager.getDefaultSharedPreferences(context)
                .getBoolean("disable_ads_key", false))) {
            if (ruleModels.size() > 3) {
                this.listItems.add(3, new ListItem(AD_VIEW));
            } else {
                this.listItems.add(new ListItem(AD_VIEW));
            }
        }

    }

    // Create new views (invoked by the layout manager)
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                      int viewType) {

        // create a new view
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.rule_item_view, parent, false);
        View adView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.advertisement_item, parent, false);

        switch (viewType) {
            case 0:
                return new RuleViewHolder(view, forwardingManager);
            case 1:
                return new AdViewHolder(adView);
        }

        return new RuleViewHolder(view, forwardingManager);
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        // - get element from your dataset at this position
        // - replace the contents of the view with that element

        switch (holder.getItemViewType()) {
            case RULE_VIEW:
                RuleViewHolder ruleHolder = (RuleViewHolder) holder;
                RuleModel ruleModel = (RuleModel) listItems.get(position).getPayload();

                //TODO: potentially should add some validation :/ / exception handling
                ruleHolder.ruleId = ruleModel.getId();
                ruleHolder.ruleProtocolText.setText(ruleModel.protocolToString());

                if (!ruleModel.isEnabled()) {
                    ruleHolder.ruleNameText.setAlpha(0.4f);
                    ruleHolder.ruleProtocolText.setBackgroundResource(R.drawable.bg_grey);
                } else {
                    ruleHolder.ruleNameText.setAlpha(1f);
                    ruleHolder.ruleProtocolText.setBackgroundResource(R.drawable.bg_red);
                }
                ruleHolder.ruleNameText.setText(ruleModel.getName());
                ruleHolder.ruleFromPortText.setText(String.valueOf(ruleModel.getFromPort()));
                ruleHolder.ruleTargetPortText.setText(String.valueOf(ruleModel.getTarget().getPort()));
                break;
            case AD_VIEW:
                AdViewHolder adHolder = (AdViewHolder) holder;
                break;
        }
    }

    @Override
    public int getItemViewType(int position) {
        // Just as an example, return 0 or 2 depending on position
        // Note that unlike in ListView adapters, types don't have to be contiguous
        return this.listItems.get(position).getViewType();
    }

    // Return the size of your dataset (invoked by the layout manager)
    // Total amount of rules + 1 for an advertisement
    @Override
    public int getItemCount() {
        return listItems.size();
    }

    private class ListItem<T> {
        private int viewType;
        private T payload;

        public ListItem(int viewType) {
            this.viewType = viewType;
        }

        public void setPayload(T payload) {
            this.payload = payload;
        }

        public T getPayload() {
            return payload;
        }

        public int getViewType() {
            return viewType;
        }
    }
} public class MainIntro extends AppIntro2 {

    // Please DO NOT override onCreate. Use init.
    @Override
    public void init(Bundle savedInstanceState) {

        // Instead of fragments, you can also use our default slide
        // Just set a title, description, background and image. AppIntro will do the rest.
        addSlide(AppIntroFragment.newInstance(getString(R.string.app_intro_1_title), getString(R.string.app_intro_1_text), R.drawable.appintro1, ContextCompat.getColor(this, R.color.colorPrimary)));
        addSlide(AppIntroFragment.newInstance(getString(R.string.app_intro_2_title), getString(R.string.app_intro_2_text), R.drawable.appintro2, ContextCompat.getColor(this, R.color.colorPrimary)));
        addSlide(AppIntroFragment.newInstance(getString(R.string.app_intro_3_title), getString(R.string.app_intro_3_text), R.drawable.appintro3, ContextCompat.getColor(this, R.color.colorPrimary)));
        showSkipButton(false);
    }

    public void showSkipButton(boolean showButton) {
        this.skipButtonEnabled = showButton;
        setButtonState(skipButton, showButton);
    }

    private void loadMainActivity() {
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
    }

    @Override
    public void onDonePressed() {
        // Do something when users tap on Done button.
        loadMainActivity();
    }

    @Override
    public void onSlideChanged() {
        // Do something when the slide changes.
    }

    @Override
    public void onNextPressed() {
        // Do something when users tap on Next button.
    }

    @Override
    public void onBackPressed() {
        // Do something when users tap on Next button.
    }

} public class HelpActivity extends BaseWebActivity {

    private static final String URL = "http://support.elix.sr/#!/products/fwd";

    public HelpActivity() {
        super(URL);
    }
} public class SettingsActivity extends BaseActivity {


    private final String TAG = "SettingsActivity";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.settings_activity);

        Toolbar toolbar = getActionBarToolbar();
        setSupportActionBar(toolbar);


        toolbar.setNavigationIcon(R.drawable.ic_arrow_back_24dp);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onBackPressed();
            }
        });

        getFragmentManager().beginTransaction().replace(R.id.content_frame, new SettingsFragment()).commit();


    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
    }
} public class NewRuleActivity extends BaseRuleActivity {

    private static final String TAG = "NewRuleActivity";
    private static final String LABEL_SAVE_RULE = "Rule Saved";
    private Tracker tracker;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.new_rule_activity);

        // Set up toolbar
        Toolbar toolbar = getActionBarToolbar();
        setSupportActionBar(toolbar);

        toolbar.setNavigationIcon(R.drawable.ic_close_24dp);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onBackPressed();
            }
        });

        constructDetailUi();

        // Get tracker.
        tracker = ((FwdApplication) this.getApplication()).getDefaultTracker();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.new_rule_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        if (id == R.id.action_save_rule) {
            Log.i(TAG, "Save Menu Button Clicked");

            // Set the item to disabled while saving
            item.setEnabled(false);
            saveNewRule();
            item.setEnabled(true);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }


    private void saveNewRule() {

        RuleModel ruleModel = generateNewRule();

        if (ruleModel.isValid()) {
            Log.i(TAG, "Rule '" + ruleModel.getName() + "' is valid, time to save.");

            // Create a DAO and save the object
            RuleDao ruleDao = new RuleDao(new RuleDbHelper(this));
            long newRowId = ruleDao.insertRule(ruleModel);

            Log.i(TAG, "Rule #" + newRowId + " '" + ruleModel.getName() + "' has been saved.");


            // Build and send an Event.
            tracker.send(new HitBuilders.EventBuilder()
                    .setCategory(CATEGORY_RULES)
                    .setAction(ACTION_SAVE)
                    .setLabel(LABEL_SAVE_RULE)
                    .build());

            // Move to main activity
            Intent mainActivityIntent = new Intent(this, MainActivity.class);
            mainActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
            startActivity(mainActivityIntent);
            finish();
        } else {
            Toast.makeText(this, "Rule is not valid. Please check your input.",
                    Toast.LENGTH_LONG).show();
        }


    }
}
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.percent.PercentRelativeLayout;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import java.util.List;
import com.elixsr.portforwarder.FwdApplication;
import com.elixsr.portforwarder.R;
import com.elixsr.portforwarder.dao.RuleDao;
import com.elixsr.portforwarder.db.RuleDbHelper;
import com.elixsr.portforwarder.forwarding.ForwardingManager;
import com.elixsr.portforwarder.forwarding.ForwardingService;
import com.elixsr.portforwarder.models.RuleModel;
import com.elixsr.portforwarder.adapters.RuleListAdapter;
import com.elixsr.portforwarder.ui.intro.MainIntro;
import com.elixsr.portforwarder.ui.preferences.HelpActivity;
import com.elixsr.portforwarder.ui.preferences.SettingsActivity;
import com.elixsr.portforwarder.ui.rules.NewRuleActivity;
import com.google.android.gms.analytics.Tracker;
import com.google.firebase.analytics.FirebaseAnalytics;
/*
 * Fwd: the port forwarding app
 * Copyright (C) 2016  Elixsr Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.elixsr.portforwarder.ui;





public class MainActivity extends BaseActivity {

    private static final String TAG = "MainActivity";
    private static final String FORWARDING_MANAGER_KEY = "ForwardingManager";
    private static final String FORWARDING_SERVICE_KEY = "ForwardingService";

    private List<RuleModel> ruleModels;
    private static RuleListAdapter ruleListAdapter;

    private RecyclerView mRecyclerView;
    private RecyclerView.Adapter mAdapter;
    private RecyclerView.LayoutManager mLayoutManager;


    private ForwardingManager forwardingManager;
    private CoordinatorLayout coordinatorLayout;
    private FloatingActionButton fab;

    private Intent forwardingServiceIntent;
    private RuleDao ruleDao;
    private PercentRelativeLayout mRuleListEmptyView;
    private Tracker tracker;

    private FirebaseAnalytics mFirebaseAnalytics;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.forwardingManager = ForwardingManager.getInstance();

        setContentView(R.layout.activity_main);
        setSupportActionBar(getActionBarToolbar());
        // getActionBarToolbar().setTitle(R.string.app_tag);
        getActionBarToolbar().setTitle("");
        getSupportActionBar().setDisplayShowHomeEnabled(true);
        getSupportActionBar().setIcon(R.drawable.ic_nav_logo);

        // Obtain the FirebaseAnalytics instance.
        mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);

        // Determine if this is first start - and whether to show app intro
        onFirstStart();

        final Intent newRuleIntent = new Intent(this, NewRuleActivity.class);

        // Move to the new rule activity
        this.fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(newRuleIntent);
            }
        });

        // Hide the fab if forwarding is enabled
        // the user should not be able to add/delete rules
        if (this.forwardingManager.isEnabled()) {
            //if the forwarding service is enabled, then we should ensure not to show the fab
            fab.hide();
        } else {
            fab.show();
        }

        // Get all models from the data store
        ruleDao = new RuleDao(new RuleDbHelper(this));
        ruleModels = ruleDao.getAllRuleModels();

        // Set up rule list and empty view
        mRecyclerView = (RecyclerView) findViewById(R.id.rule_recycler_view);
        mRuleListEmptyView = (PercentRelativeLayout) findViewById(R.id.rule_list_empty_view);

        // Use this setting to improve performance if you know that changes
        // In content do not change the layout size of the RecyclerView
        mRecyclerView.setHasFixedSize(true);

        // Use a linear layout manager
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);

        // Specify an adapter (see also next example)
        ruleListAdapter = new RuleListAdapter(ruleModels, forwardingManager, getApplicationContext());
        mRecyclerView.setAdapter(ruleListAdapter);

        // Store the coordinator layout for snackbar
        coordinatorLayout = (CoordinatorLayout) findViewById(R.id.main_coordinator_layout);


        forwardingServiceIntent = new Intent(this, ForwardingService.class);


        /*
            Service stuff
         */

        // Handle intents
        IntentFilter mStatusIntentFilter = new IntentFilter(
                ForwardingService.BROADCAST_ACTION);

        // Instantiates a new ForwardingServiceResponseReceiver
        ForwardingServiceResponseReceiver forwardingServiceResponseReceiver =
                new ForwardingServiceResponseReceiver();

        // Registers the ForwardingServiceResponseReceiver and its intent filters
        LocalBroadcastManager.getInstance(this).registerReceiver(
                forwardingServiceResponseReceiver,
                mStatusIntentFilter);

        // Get tracker.
        tracker = ((FwdApplication) this.getApplication()).getDefaultTracker();