public class Controller {
    public static final byte MAX_CONTROLLER_VALUES = 20;
    public static final byte MAX_EXPANSION_RELAYS = 8;
    public static final byte MAX_RELAY_PORTS = 8;
    public static final byte MAX_TEMP_SENSORS = 3;
    public static final byte MAX_PWM_EXPANSION_PORTS = 6;
    public static final byte MAX_AI_CHANNELS = 3;
    public static final byte MAX_CUSTOM_VARIABLES = 8;
    public static final byte MAX_IO_CHANNELS = 6;
    public static final byte MAX_RADION_LIGHT_CHANNELS = 6;
    public static final byte MAX_VORTECH_VALUES = 3;
    public static final byte MAX_WATERLEVEL_PORTS = 5;
    public static final byte MAX_STATUS_FLAGS = 3;
    public static final byte MAX_ALERT_FLAGS = 4;
    public static final byte MAX_SCPWM_EXPANSION_PORTS = 16;
    public static final byte MAX_DCPUMP_VALUES = 4;

    // First set of expansion modules - EM
    public static final short MODULE_DIMMING = 1;
    public static final short MODULE_RF = 1 << 1;
    public static final short MODULE_AI = 1 << 2;
    public static final short MODULE_SALINITY = 1 << 3;
    public static final short MODULE_ORP = 1 << 4;
    public static final short MODULE_IO = 1 << 5;
    public static final short MODULE_PHEXPANSION = 1 << 6;
    public static final short MODULE_WATERLEVEL = 1 << 7;

    // Second set of expansion modules - EM1
    public static final short MODULE_HUMIDITY = 1;
    public static final short MODULE_DCPUMP = 1 << 1;
    public static final short MODULE_LEAKDETECTOR = 1 << 2;
    public static final short MODULE_PAR = 1 << 3;
    public static final short MODULE_SCPWM = 1 << 4;  // 16 channel pwm module

    // Status flags - SF
    public static final short SF_LIGHTSON = 1;
    public static final short SF_FEEDING = 1 << 1;
    public static final short SF_WATERCHANGE = 1 << 2;

    // Alert flags - AF
    public static final short AF_ATOTIMEOUT = 1;
    public static final short AF_OVERHEAT = 1 << 1;
    public static final short AF_BUSLOCK = 1 << 2;
    public static final short AF_LEAK = 1 << 3;

    // AI channels
    public static final byte AI_WHITE = 0;
    public static final byte AI_BLUE = 1;
    public static final byte AI_ROYALBLUE = 2;

    // Radion channels
    public static final byte RADION_WHITE = 0;
    public static final byte RADION_ROYALBLUE = 1;
    public static final byte RADION_RED = 2;
    public static final byte RADION_GREEN = 3;
    public static final byte RADION_BLUE = 4;
    public static final byte RADION_INTENSITY = 5;

    // Vortech modes
    public static final byte VORTECH_MODE = 0;
    public static final byte VORTECH_SPEED = 1;
    public static final byte VORTECH_DURATION = 2;

    // DCPump modes
    public static final byte DCPUMP_MODE = 0;
    public static final byte DCPUMP_SPEED = 1;
    public static final byte DCPUMP_DURATION = 2;
    public static final byte DCPUMP_THRESHOLD = 3;

    // Board IDs
    public static final byte RA = 0;
    public static final byte RAPlus = 1;
    public static final byte RATouchDisplay = 2;
    public static final byte RATouch = 3;
    public static final byte RAStar = 4;
    public static final byte RAEvolution = 5;

    private String updateLogDate;
    private NumberWithLabel[] tempSensors;
    private NumberWithLabel pH;
    private NumberWithLabel pHExp;
    private boolean atoLow;
    private boolean atoHigh;
    private ShortWithLabelOverride pwmA;
    private ShortWithLabelOverride pwmD;
    private ShortWithLabelOverride pwmA2;
    private ShortWithLabelOverride pwmD2;
    private ShortWithLabel[] waterlevel;
    private ShortWithLabel humidity;
    private NumberWithLabel salinity;
    private NumberWithLabel orp;
    private Relay main;
    private byte qtyExpansionRelays;
    private Relay[] expansionRelays;
    private short expansionModules;
    private short expansionModules1;
    private short relayExpansionModules;
    private short flagsAlert;
    private short flagsStatus;
    private short ioChannels;
    private String[] ioChannelsLabels;
    private ShortWithLabelOverride[] pwmExpansion;
    private ShortWithLabelOverride[] aiChannels;
    private ShortWithLabelOverride[] radionChannels;
    private ShortWithLabel[] customVariables;
    private short[] vortechValues;
    private ShortWithLabelOverride[] pwmSCExpansion;
    private short[] dcPumpValues;
    private NumberWithLabel par;
    private byte board;

    public Controller () {
        init();
    }

    public Controller ( byte numExpRelays ) {
        init();
        this.qtyExpansionRelays = numExpRelays;
    }

    private void init ( ) {
        updateLogDate = "";
        tempSensors = new NumberWithLabel[MAX_TEMP_SENSORS];
        int i;
        for ( i = 0; i < MAX_TEMP_SENSORS; i++ ) {
            tempSensors[i] = new NumberWithLabel( (byte) 1 );
        }
        pH = new NumberWithLabel( (byte) 2 );
        pHExp = new NumberWithLabel( (byte) 2 );
        atoLow = false;
        atoHigh = false;
        pwmA = new ShortWithLabelOverride();
        pwmD = new ShortWithLabelOverride();
        pwmA2 = new ShortWithLabelOverride();
        pwmD2 = new ShortWithLabelOverride();
        pwmExpansion = new ShortWithLabelOverride[MAX_PWM_EXPANSION_PORTS];
        for ( i = 0; i < MAX_PWM_EXPANSION_PORTS; i++ ) {
            pwmExpansion[i] = new ShortWithLabelOverride();
        }
        waterlevel = new ShortWithLabel[MAX_WATERLEVEL_PORTS];
        for ( i = 0; i < MAX_WATERLEVEL_PORTS; i++ ) {
            waterlevel[i] = new ShortWithLabel();
        }
        humidity = new ShortWithLabel();
        salinity = new NumberWithLabel( (byte) 1 );
        orp = new NumberWithLabel();
        par = new NumberWithLabel();
        board = RAPlus;  // default to RAPlus board
        main = new Relay();
        expansionRelays = new Relay[MAX_EXPANSION_RELAYS];
        for ( i = 0; i < MAX_EXPANSION_RELAYS; i++ ) {
            expansionRelays[i] = new Relay();
        }
        qtyExpansionRelays = 0;
        expansionModules = 0;
        expansionModules1 = 0;
        relayExpansionModules = 0;
        flagsStatus = 0;
        flagsAlert = 0;
        ioChannels = 0;
        ioChannelsLabels = new String[MAX_IO_CHANNELS];
        for ( i = 0; i < MAX_IO_CHANNELS; i++ ) {
            ioChannelsLabels[i] = "";
        }
        aiChannels = new ShortWithLabelOverride[MAX_AI_CHANNELS];
        for ( i = 0; i < MAX_AI_CHANNELS; i++ ) {
            aiChannels[i] = new ShortWithLabelOverride();
        }
        radionChannels = new ShortWithLabelOverride[MAX_RADION_LIGHT_CHANNELS];
        for ( i = 0; i < MAX_RADION_LIGHT_CHANNELS; i++ ) {
            radionChannels[i] = new ShortWithLabelOverride();
        }
        customVariables = new ShortWithLabel[MAX_CUSTOM_VARIABLES];
        for ( i = 0; i < MAX_CUSTOM_VARIABLES; i++ ) {
            customVariables[i] = new ShortWithLabel();
        }
        vortechValues = new short[MAX_VORTECH_VALUES];
        for ( i = 0; i < MAX_VORTECH_VALUES; i++ ) {
            vortechValues[i] = 0;
        }
        pwmSCExpansion = new ShortWithLabelOverride[MAX_SCPWM_EXPANSION_PORTS];
        for ( i = 0; i < MAX_SCPWM_EXPANSION_PORTS; i++ ) {
            pwmSCExpansion[i] = new ShortWithLabelOverride();
        }
        dcPumpValues = new short[MAX_DCPUMP_VALUES];
        for ( i = 0; i < MAX_DCPUMP_VALUES; i++ ) {
            dcPumpValues[i] = 0;
        }
    }

    public void setNumExpansionRelays ( byte relays ) {
        qtyExpansionRelays = relays;
    }

    public byte getNumExpansionRelays ( ) {
        return qtyExpansionRelays;
    }

    public void setLogDate ( String date ) {
        updateLogDate = date;
    }

    public String getLogDate ( ) {
        return updateLogDate;
    }

    public void setTempLabel ( int sensor, String label ) {
        // sanity check: make sure the sensor is within range
        if ((sensor < 1) || (sensor > MAX_TEMP_SENSORS))
            return;
        tempSensors[sensor - 1].setLabel( label );
    }

    public String getTempLabel ( int sensor ) {
        // sanity check: make sure the sensor is within range
        if ((sensor < 1) || (sensor > MAX_TEMP_SENSORS))
            return "";
        return tempSensors[sensor - 1].getLabel();
    }

    public void setTemp1 ( int value ) {
        tempSensors[0].setData( value );
    }

    public String getTemp1 ( ) {
        return tempSensors[0].getData();
    }

    public void setTemp2 ( int value ) {
        tempSensors[1].setData( value );
    }

    public String getTemp2 ( ) {
        return tempSensors[1].getData();
    }

    public void setTemp3 ( int value ) {
        tempSensors[2].setData( value );
    }

    public String getTemp3 ( ) {
        return tempSensors[2].getData();
    }

    public void setPH ( int value ) {
        pH.setData( value );
    }

    public String getPH ( ) {
        return pH.getData();
    }

    public void setPHLabel ( String label ) {
        pH.setLabel( label );
    }

    public String getPHLabel ( ) {
        return pH.getLabel();
    }

    public void setPHExp ( int value ) {
        pHExp.setData( value );
    }

    public String getPHExp ( ) {
        return pHExp.getData();
    }

    public void setPHExpLabel ( String label ) {
        pHExp.setLabel( label );
    }

    public String getPHExpLabel ( ) {
        return pHExp.getLabel();
    }

    public void setAtoLow ( boolean v ) {
        atoLow = v;
    }

    public boolean getAtoLow ( ) {
        return atoLow;
    }

    public String getAtoLowText ( ) {
        return getAtoText( atoLow );
    }

    private String getAtoText ( boolean active ) {
        // TODO use strings.xml instead of hard code
        if ( active )
            return "ON";
        else
            return "OFF";
    }

    public void setAtoHigh ( boolean v ) {
        atoHigh = v;
    }

    public boolean getAtoHigh ( ) {
        return atoHigh;
    }

    public String getAtoHighText ( ) {
        return getAtoText( atoHigh );
    }

    public void setPwmA ( short v ) {
        pwmA.setData( v );
    }

    public short getPwmA ( ) {
        return pwmA.getData();
    }

    public short getPwmAOverride ( ) {
        return pwmA.getOverrideValue();
    }

    public void setPwmAOverride ( short v ) {
        pwmA.setOverride( v );
    }

    public void setPwmALabel ( String label ) {
        pwmA.setLabel( label );
    }

    public String getPwmALabel ( ) {
        return pwmA.getLabel();
    }

    public void setPwmD ( short v ) {
        pwmD.setData( v );
    }

    public short getPwmD ( ) {
        return pwmD.getData();
    }

    public short getPwmDOverride ( ) {
        return pwmD.getOverrideValue();
    }

    public void setPwmDOverride ( short v ) {
        pwmD.setOverride( v );
    }

    public void setPwmDLabel ( String label ) {
        pwmD.setLabel( label );
    }

    public String getPwmDLabel ( ) {
        return pwmD.getLabel();
    }

    public void setPwmA2 ( short v ) {
        pwmA2.setData( v );
    }

    public short getPwmA2 ( ) {
        return pwmA2.getData();
    }

    public short getPwmA2Override ( ) {
        return pwmA2.getOverrideValue();
    }

    public void setPwmA2Override ( short v ) {
        pwmA2.setOverride( v );
    }

    public void setPwmA2Label ( String label ) {
        pwmA2.setLabel( label );
    }

    public String getPwmA2Label ( ) {
        return pwmA2.getLabel();
    }

    public void setPwmD2 ( short v ) {
        pwmD2.setData( v );
    }

    public short getPwmD2 ( ) {
        return pwmD2.getData();
    }

    public short getPwmD2Override ( ) {
        return pwmD2.getOverrideValue();
    }

    public void setPwmD2Override ( short v ) {
        pwmD2.setOverride( v );
    }

    public void setPwmD2Label ( String label ) {
        pwmD2.setLabel( label );
    }

    public String getPwmD2Label ( ) {
        return pwmD2.getLabel();
    }

    public void setPwmExpansion ( short channel, short v ) {
        if ((channel < 0) || (channel > MAX_PWM_EXPANSION_PORTS))
            return;
        pwmExpansion[channel].setData( v );
    }

    public short getPwmExpansion ( short channel ) {
        if ((channel < 0) || (channel > MAX_PWM_EXPANSION_PORTS))
            return 0;
        return pwmExpansion[channel].getData();
    }

    public short getPwmExpansionOverride ( short channel ) {
        if ((channel < 0) || (channel > MAX_PWM_EXPANSION_PORTS))
            return Globals.OVERRIDE_DISABLE;
        return pwmExpansion[channel].getOverrideValue();
    }

    public void setPwmExpansionOverride ( short channel, short v ) {
        if ((channel < 0) || (channel > MAX_PWM_EXPANSION_PORTS))
            return;
        pwmExpansion[channel].setOverride( v );
    }

    public void setPwmExpansionLabel ( short channel, String label ) {
        if ((channel < 0) || (channel > MAX_PWM_EXPANSION_PORTS))
            return;
        pwmExpansion[channel].setLabel( label );
    }

    public String getPwmExpansionLabel ( short channel ) {
        if ((channel < 0) || (channel > MAX_PWM_EXPANSION_PORTS))
            return "";
        return pwmExpansion[channel].getLabel();
    }

    public void setSCPwmExpansion ( short channel, short v ) {
        if ((channel < 0) || (channel > MAX_SCPWM_EXPANSION_PORTS))
            return;
        pwmSCExpansion[channel].setData(v);
    }

    public short getSCPwmExpansion ( short channel ) {
        if ((channel < 0) || (channel > MAX_SCPWM_EXPANSION_PORTS))
            return 0;
        return pwmSCExpansion[channel].getData();
    }

    public short getSCPwmExpansionOverride ( short channel ) {
        if ((channel < 0) || (channel > MAX_SCPWM_EXPANSION_PORTS))
            return Globals.OVERRIDE_DISABLE;
        return pwmSCExpansion[channel].getOverrideValue();
    }

    public void setSCPwmExpansionOverride ( short channel, short v ) {
        if ((channel < 0) || (channel > MAX_SCPWM_EXPANSION_PORTS))
            return;
        pwmSCExpansion[channel].setOverride(v);
    }

    public void setSCPwmExpansionLabel ( short channel, String label ) {
        if ((channel < 0) || (channel > MAX_SCPWM_EXPANSION_PORTS))
            return;
        pwmSCExpansion[channel].setLabel(label);
    }

    public String getSCPwmExpansionLabel ( short channel ) {
        if ((channel < 0) || (channel > MAX_SCPWM_EXPANSION_PORTS))
            return "";
        return pwmSCExpansion[channel].getLabel();
    }

    public void setWaterLevel ( short port, short value ) {
        if ((port < 0) || (port > MAX_WATERLEVEL_PORTS))
            return;
        waterlevel[port].setData( value );
    }

    public short getWaterLevel ( short port ) {
        if ((port < 0) || (port > MAX_WATERLEVEL_PORTS))
            return 0;
        return waterlevel[port].getData();
    }

    public void setWaterLevelLabel ( short port, String label ) {
        if ((port < 0) || (port > MAX_WATERLEVEL_PORTS))
            return;
        waterlevel[port].setLabel( label );
    }

    public String getWaterLevelLabel ( short port ) {
        if ((port < 0) || (port > MAX_WATERLEVEL_PORTS))
            return "";
        return waterlevel[port].getLabel();
    }

    public void setHumidity ( short value ) {
        humidity.setData( value );
    }

    public short getHumidity ( ) {
        return humidity.getData();
    }

    public void setHumidityLabel ( String label ) {
        humidity.setLabel( label );
    }

    public String getHumidityLabel ( ) {
        return humidity.getLabel();
    }

    public void setSalinity ( int value ) {
        salinity.setData( value );
    }

    public String getSalinity ( ) {
        return salinity.getData();
    }

    public void setSalinityLabel ( String label ) {
        salinity.setLabel( label );
    }

    public String getSalinityLabel ( ) {
        return salinity.getLabel();
    }

    public void setORP ( int value ) {
        orp.setData( value );
    }

    public String getORP ( ) {
        return orp.getData();
    }

    public void setORPLabel ( String label ) {
        orp.setLabel( label );
    }

    public String getORPLabel ( ) {
        return orp.getLabel();
    }

    public void setPar ( int value ) {
        par.setData( value );
    }

    public String getPar ( ) {
        return par.getData();
    }

    public void setParLabel ( String label ) {
        par.setLabel( label );
    }

    public String getParLabel ( ) {
        return par.getLabel();
    }

    public void setBoard ( byte bid ) {
        board = bid;
    }

    public byte getBoard ( ) {
        return board;
    }

    public boolean isRAStar() {
        return board == RAPlus;
    }

    public void setMainRelayData ( short data, short maskOn, short maskOff ) {
        main.setRelayData( data, maskOn, maskOff );
    }

    public void setMainRelayData ( short data ) {
        main.setRelayData( data );
    }

    public void setMainRelayOnMask ( short maskOn ) {
        main.setRelayOnMask( maskOn );
    }

    public void setMainRelayOffMask ( short maskOff ) {
        main.setRelayOffMask( maskOff );
    }

    public Relay getMainRelay ( ) {
        return main;
    }

    public void setExpRelayData ( int relay, short data ) {
        // Pass in the 1 based index for relay
        expansionRelays[relay - 1].setRelayData( data );
    }

    public void setExpRelayOnMask ( int relay, short maskOn ) {
        // Pass in the 1 based index for relay
        expansionRelays[relay - 1].setRelayOnMask( maskOn );
    }

    public void setExpRelayOffMask ( int relay, short maskOff ) {
        // Pass in the 1 based index for relay
        expansionRelays[relay - 1].setRelayOffMask( maskOff );
    }

    public Relay getExpRelay ( int relay ) {
        return expansionRelays[relay - 1];
    }

    public short getCustomVariable ( short var ) {
        if ((var < 0) || (var > MAX_CUSTOM_VARIABLES))
            return 0;
        return customVariables[var].getData();
    }

    public void setCustomVariable ( short var, short value ) {
        if ((var < 0) || (var > MAX_CUSTOM_VARIABLES))
            return;
        customVariables[var].setData( value );
    }

    public String getCustomVariableLabel ( short var ) {
        if ((var < 0) || (var > MAX_CUSTOM_VARIABLES))
            return "";
        return customVariables[var].getLabel();
    }

    public void setCustomVariableLabel ( short var, String label ) {
        if ((var < 0) || (var > MAX_CUSTOM_VARIABLES))
            return;
        customVariables[var].setLabel( label );
    }

    public short getAIChannel ( byte channel ) {
        return aiChannels[channel].getData();
    }

    public void setAIChannel ( byte channel, short value ) {
        aiChannels[channel].setData( value );
    }

    public short getAIChannelOverride ( byte channel ) {
        return aiChannels[channel].getOverrideValue();
    }

    public void setAIChannelOverride ( byte channel, short value ) {
        aiChannels[channel].setOverride( value );
    }

    public short getRadionChannel ( byte channel ) {
        return radionChannels[channel].getData();
    }

    public void setRadionChannel ( byte channel, short value ) {
        radionChannels[channel].setData( value );
    }

    public short getRadionChannelOverride ( byte channel ) {
        return radionChannels[channel].getOverrideValue();
    }

    public void setRadionChannelOverride ( byte channel, short value ) {
        radionChannels[channel].setOverride( value );
    }

    public short getVortechValue ( byte type ) {
        return vortechValues[type];
    }

    public void setVortechValue ( byte type, short value ) {
        vortechValues[type] = value;
    }

    public short getDCPumpValue ( byte type ) {
        return dcPumpValues[type];
    }

    public void setDCPumpValue ( byte type, short value ) {
        dcPumpValues[type] = value;
    }

    public short getExpansionModules ( ) {
        return expansionModules;
    }

    public void setExpansionModules ( short em ) {
        expansionModules = em;
    }

    public short getExpansionModules1 ( ) {
        return expansionModules1;
    }

    public void setExpansionModules1 ( short em1 ) {
        expansionModules1 = em1;
    }

    public static boolean isDimmingModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_DIMMING) == MODULE_DIMMING;
    }

    public static boolean isRFModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_RF) == MODULE_RF;
    }

    public static boolean isAIModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_AI) == MODULE_AI;
    }

    public static boolean isSalinityModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_SALINITY) == MODULE_SALINITY;
    }

    public static boolean isORPModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_ORP) == MODULE_ORP;
    }

    public static boolean isIOModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_IO) == MODULE_IO;
    }

    public static boolean isPHExpansionModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_PHEXPANSION) == MODULE_PHEXPANSION;
    }

    public static boolean isWaterLevelModuleInstalled ( short expansionModules ) {
        return (expansionModules & MODULE_WATERLEVEL) == MODULE_WATERLEVEL;
    }

    public static boolean isHumidityModuleInstalled ( short expansionModules1 ) {
        return (expansionModules1 & MODULE_HUMIDITY) == MODULE_HUMIDITY;
    }

    public static boolean isDCPumpControlModuleInstalled ( short expansionModules1 ) {
        return (expansionModules1 & MODULE_DCPUMP) == MODULE_DCPUMP;
    }

    public static boolean isLeakDetectorModuleInstalled ( short expansionModules1 ) {
        return (expansionModules1 & MODULE_LEAKDETECTOR) == MODULE_LEAKDETECTOR;
    }

    public static boolean isParModuleInstalled ( short expansionModules1 ) {
        return (expansionModules1 & MODULE_PAR) == MODULE_PAR;
    }

    public short getRelayExpansionModules ( ) {
        return relayExpansionModules;
    }

    public void setRelayExpansionModules ( short rem ) {
        relayExpansionModules = rem;
    }

    public static int getRelayExpansionModulesInstalled ( short rem ) {
        int qty = 0;
        for ( int i = 7; i >= 0; i-- ) {
            if ( (rem & (1 << i)) == (1 << i) ) {
                qty = i + 1;
                break;
            }
        }
        return qty;
    }

    public void setStatusFlags ( short flags ) {
        flagsStatus = flags;
    }

    public short getStatusFlags ( ) {
        return flagsStatus;
    }

    public static boolean isLightsOnFlagSet ( short flags ) {
        return (flags & SF_LIGHTSON) == SF_LIGHTSON;
    }

    public static boolean isFeedingFlagSet ( short flags ) {
        return (flags & SF_FEEDING) == SF_FEEDING;
    }

    public static boolean isWaterChangeFlagSet ( short flags ) {
        return (flags & SF_WATERCHANGE) == SF_WATERCHANGE;
    }

    public void setAlertFlags ( short flags ) {
        flagsAlert = flags;
    }

    public short getAlertFlags ( ) {
        return flagsAlert;
    }

    public static boolean isATOTimeoutFlagSet ( short flags ) {
        return (flags & AF_ATOTIMEOUT) == AF_ATOTIMEOUT;
    }

    public static boolean isOverheatFlagSet ( short flags ) {
        return (flags & AF_OVERHEAT) == AF_OVERHEAT;
    }

    public static boolean isBusLockFlagSet ( short flags ) {
        return (flags & AF_BUSLOCK) == AF_BUSLOCK;
    }

    public static boolean isLeakFlagSet ( short flags ) {
        return (flags & AF_LEAK) == AF_LEAK;
    }

    public short getIOChannels ( ) {
        return ioChannels;
    }

    public void setIOChannels ( short ioChannels ) {
        this.ioChannels = ioChannels;
    }

    public String getIOChannelLabel ( short channel ) {
        return ioChannelsLabels[channel];
    }

    public void setIOChannelLabel ( short channel, String label ) {
        ioChannelsLabels[channel] = label;
    }

    public static boolean getIOChannel ( short ioChannels, byte channel ) {
        // channel is 0 based
        int v = (1 << channel);
        int w = (ioChannels & v);
        boolean f = w == v;
        return f;
    }

    public static boolean isPWMOverrideEnabled( short value, short override ) {
        // above 100% (max value), so the override is disabled
        if ( override > Globals.OVERRIDE_MAX_VALUE ) {
            return false;
        }
        return true;
    }

    public static short getPWMValueFromOverride ( short data, short override ) {
        short v;
        if ( isPWMOverrideEnabled(data, override) ) {
            v = override;
        } else {
            v = data;
        }
        //Log.d("getPWMValueFromOverride", "data: " + data + " override: " + override + " return: " + v);
        return v;
    }

    public static float getPWMValueFromOverride( float data, float override) {
        float v;
        // above 100%, so override is disabled
        if ( override > Globals.OVERRIDE_MAX_VALUE ) {
            // override disabled, return actual value
            v = data;
        } else {
            // override is enabled, so we return override value
            v = override;
        }
        return v;
    }

    public static String getPWMDisplayValue( short data, short override ) {
        String s = String.format(Locale.getDefault(), "%d%%", getPWMValueFromOverride(data, override));
        if ( isPWMOverrideEnabled(data, override) ) {
            s = "**" + s;
        }
        return s;
    }
} public class ErrorTable {

	// private static final String TAG = ErrorTable.class.getSimpleName();

	// Database constants
	public static final String TABLE_NAME = "errors";
	// columns
	public static final String COL_ID = "_id";
	public static final String COL_TIME = "time";
	public static final String COL_MESSAGE = "message";
	public static final String COL_READ = "read";

	private static final String CREATE_TABLE =
			"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + COL_ID
					+ " INTEGER PRIMARY KEY AUTOINCREMENT, " + COL_TIME
					+ " INTEGER, " + COL_MESSAGE + " TEXT, " + COL_READ
					+ " INTEGER);";
	private static final String DROP_TABLE = "DROP TABLE IF EXISTS "
												+ TABLE_NAME;

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

	public static void onUpgrade (
			SQLiteDatabase db,
			int oldVersion,
			int newVersion ) {
        int curVer = oldVersion;
        while ( curVer < newVersion ) {
            curVer++;
            switch ( curVer ) {
                default:
                    break;
                case 5:
                    upgradeToVersion5(db);
                    break;
            }
        }
	}

    private static void upgradeToVersion5 ( SQLiteDatabase db ) {
        // table did not exist prior to version 5
        dropTable(db);
        onCreate(db);
    }

	private static void dropTable ( SQLiteDatabase db ) {
		db.execSQL( DROP_TABLE );
	}

	public static void onDowngrade (
			SQLiteDatabase db,
			int oldVersion,
			int newVersion ) {
        int curVer = oldVersion;
        while ( curVer > newVersion ) {
            curVer--;
            switch ( curVer ) {
                default:
                    break;
                case 4:
                case 3:
                case 2:
                case 1:
                    // drop the table if the downgraded version is less than 5
                    dropTable(db);
                    break;
            }
        }
	}
} public class NotificationTable {
	// Database constants
	public static final String TABLE_NAME = "notifications";
	// columns
	public static final String COL_ID = "_id";
	public static final String COL_PARAM = "param";
	public static final String COL_CONDITION = "condition";
	public static final String COL_VALUE = "value";

	private static final String CREATE_TABLE =
			"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + COL_ID
					+ " INTEGER PRIMARY KEY AUTOINCREMENT, " + COL_PARAM
					+ " INTEGER, " + COL_CONDITION + " INTEGER, " + COL_VALUE
					+ " TEXT);";
	private static final String DROP_TABLE = "DROP TABLE IF EXISTS "
												+ TABLE_NAME;

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

	public static void onUpgrade (
			SQLiteDatabase db,
			int oldVersion,
			int newVersion ) {
        int curVer = oldVersion;
        while ( curVer < newVersion ) {
            curVer++;
            switch (curVer) {
                default:
                    break;
                case 6:
                    upgradeToVersion6(db);
                    break;
            }
        }
	}

    private static void upgradeToVersion6 ( SQLiteDatabase db ) {
        // table did no exist prior to version 6, so just drop it as a safety precaution
        // then create it
        dropTable(db);
        onCreate(db);
    }

	private static void dropTable ( SQLiteDatabase db ) {
		db.execSQL( DROP_TABLE );
	}

	public static void onDowngrade (
			SQLiteDatabase db,
			int oldVersion,
			int newVersion ) {
        int curVer = oldVersion;
        while ( curVer > newVersion ) {
            curVer--;
            switch ( curVer ) {
                default:
                    break;
                case 5:
                case 4:
                case 3:
                case 2:
                case 1:
                    // drop the table if the downgraded version is less than 6
                    dropTable(db);
                    break;
            }
        }
	}
} public class StatusProvider extends ContentProvider {

	// private static String TAG = StatusProvider.class.getSimpleName();
	private RADbHelper data;

    private static final String CONTENT = buildAuthority();

    private static String buildAuthority() {
        String a = StatusProvider.class.getPackage().getName();
        if (BuildConfig.DEBUG) {
            a += ".debug";
        }
        return a;
    }

	private static final String CONTENT_MIME_TYPE = "/vnd." + CONTENT + ".";
	public static final Uri CONTENT_URI = Uri.parse( "content://" + CONTENT );

	// PATHS
	public static final String PATH_LATEST = "latest";
	public static final String PATH_STATUS = "status";
	public static final String PATH_ERROR = "error";
	public static final String PATH_NOTIFICATION = "notification";
	public static final String PATH_CONTROLLER = "controller";
	public static final String PATH_USER_MEMORY = "usermemory";

	// MIME Types
	// latest item
	public static final String LATEST_MIME_TYPE =
			ContentResolver.CURSOR_ITEM_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_LATEST;
	// status - item
	public static final String STATUS_ID_MIME_TYPE =
			ContentResolver.CURSOR_ITEM_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_STATUS;
	// status - all items
	public static final String STATUS_MIME_TYPE =
			ContentResolver.CURSOR_DIR_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_STATUS;
	// error - item (not sure if needed)
	public static final String ERROR_ID_MIME_TYPE =
			ContentResolver.CURSOR_ITEM_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_ERROR;
	// error - all items
	public static final String ERROR_MIME_TYPE =
			ContentResolver.CURSOR_DIR_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_ERROR;
	// notification - item
	public static final String NOTIFICATION_ID_MIME_TYPE =
			ContentResolver.CURSOR_ITEM_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_NOTIFICATION;
	// notification - all items
	public static final String NOTIFICATION_MIME_TYPE =
			ContentResolver.CURSOR_DIR_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_NOTIFICATION;
	// controller - 1 item
	public static final String CONTROLLER_ID_MIME_TYPE =
			ContentResolver.CURSOR_ITEM_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_CONTROLLER;
	// controller - all items
	public static final String CONTROLLER_MIME_TYPE =
			ContentResolver.CURSOR_DIR_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_CONTROLLER;
	// usermemory - 1 item
	public static final String USER_MEMORY_ID_MIME_TYPE =
			ContentResolver.CURSOR_ITEM_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_USER_MEMORY;
	// usermemory = all items
	public static final String USER_MEMORY_MIME_TYPE =
			ContentResolver.CURSOR_DIR_BASE_TYPE + CONTENT_MIME_TYPE
					+ PATH_USER_MEMORY;

	// Used for the UriMatcher
	private static final int CODE_LATEST = 10;
	private static final int CODE_STATUS = 11;
	private static final int CODE_STATUS_ID = 12;
	private static final int CODE_ERROR = 13;
	private static final int CODE_ERROR_ID = 14;
	private static final int CODE_NOTIFICATION = 15;
	private static final int CODE_NOTIFICATION_ID = 16;
	private static final int CODE_USER_MEMORY = 17;
	private static final int CODE_USER_MEMORY_ID = 18;
	private static final int CODE_CONTROLLER = 20;  // all controllers
	private static final int CODE_CONTROLLER_ID = 21; // individual controllers

	private static final UriMatcher sUriMatcher = new UriMatcher(
		UriMatcher.NO_MATCH );
	static {
		sUriMatcher.addURI( CONTENT, PATH_LATEST, CODE_LATEST );
		sUriMatcher.addURI( CONTENT, PATH_STATUS, CODE_STATUS );
		sUriMatcher.addURI( CONTENT, PATH_STATUS + "/#", CODE_STATUS_ID );
		sUriMatcher.addURI( CONTENT, PATH_ERROR, CODE_ERROR );
		sUriMatcher.addURI( CONTENT, PATH_ERROR + "/#", CODE_ERROR_ID );
		sUriMatcher.addURI( CONTENT, PATH_NOTIFICATION, CODE_NOTIFICATION );
		sUriMatcher.addURI( CONTENT, PATH_NOTIFICATION + "/#",
							CODE_NOTIFICATION_ID );
		sUriMatcher.addURI( CONTENT, PATH_CONTROLLER, CODE_CONTROLLER );
		sUriMatcher.addURI( CONTENT, PATH_CONTROLLER + "/#", CODE_CONTROLLER_ID );
		sUriMatcher.addURI( CONTENT, PATH_USER_MEMORY, CODE_USER_MEMORY );
		sUriMatcher.addURI( CONTENT, PATH_USER_MEMORY + "/#", CODE_USER_MEMORY_ID );
	}

	@Override
	public boolean onCreate ( ) {
		data = new RADbHelper( getContext() );
		return ((data == null) ? false : true);
	}

	@Override
	public Cursor query (
			Uri uri,
			String[] projection,
			String selection,
			String[] selectionArgs,
			String sortOrder ) {

		String limit = null;
		String table = null;
		SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
		switch ( sUriMatcher.match( uri ) ) {
			case CODE_LATEST:
				// limit to the most recent entry
				limit = "1";
				table = StatusTable.TABLE_NAME;
				break;
			case CODE_STATUS:
				// no limits, returns all the data in reverse order
				// the passed in query will be the limiting factor
				table = StatusTable.TABLE_NAME;
				break;
			case CODE_STATUS_ID:
				// only get the specified id
				table = StatusTable.TABLE_NAME;
				qb.appendWhere( StatusTable.COL_ID + "="
								+ uri.getLastPathSegment() );
				break;
			case CODE_ERROR: // no limits
				table = ErrorTable.TABLE_NAME;
				break;
			case CODE_ERROR_ID:
				table = ErrorTable.TABLE_NAME;
				qb.appendWhere( ErrorTable.COL_ID + "="
								+ uri.getLastPathSegment() );
				break;
			case CODE_NOTIFICATION: // no limits
				table = NotificationTable.TABLE_NAME;
				break;
			case CODE_NOTIFICATION_ID:
				table = NotificationTable.TABLE_NAME;
				qb.appendWhere( NotificationTable.COL_ID + "="
								+ uri.getLastPathSegment() );
				break;
			case CODE_USER_MEMORY:
				table = UserMemoryLocationsTable.TABLE_NAME;
				break;
			case CODE_USER_MEMORY_ID:
				table = UserMemoryLocationsTable.TABLE_NAME;
				qb.appendWhere( UserMemoryLocationsTable.COL_ID + "="
						+ uri.getLastPathSegment() );
				break;
            case CODE_CONTROLLER: // no limits
                table = ControllersTable.TABLE_NAME;
                break;
            case CODE_CONTROLLER_ID:
                table = ControllersTable.TABLE_NAME;
                qb.appendWhere( ControllersTable.COL_CONTROLLER_ID + "="
                                + uri.getLastPathSegment() );
                break;
			default:
				throw new IllegalArgumentException( "Uknown URI: " + uri );
		}
		qb.setTables( table );
		SQLiteDatabase db = data.getWritableDatabase();
		Cursor c =
				qb.query(	db, projection, selection, selectionArgs, null,
							null,
							sortOrder, limit );
		c.setNotificationUri( getContext().getContentResolver(), uri );
		return c;
	}

	@Override
	public String getType ( Uri uri ) {
		int match = sUriMatcher.match( uri );
		switch ( match ) {
			case CODE_LATEST:
				return LATEST_MIME_TYPE;
			case CODE_STATUS:
				return STATUS_MIME_TYPE;
			case CODE_STATUS_ID:
				return STATUS_ID_MIME_TYPE;
			case CODE_ERROR:
				return ERROR_MIME_TYPE;
			case CODE_ERROR_ID:
				return ERROR_ID_MIME_TYPE;
			case CODE_NOTIFICATION:
				return NOTIFICATION_MIME_TYPE;
			case CODE_NOTIFICATION_ID:
				return NOTIFICATION_ID_MIME_TYPE;
			case CODE_USER_MEMORY:
				return USER_MEMORY_MIME_TYPE;
			case CODE_USER_MEMORY_ID:
				return USER_MEMORY_ID_MIME_TYPE;
            case CODE_CONTROLLER:
                return CONTROLLER_MIME_TYPE;
            case CODE_CONTROLLER_ID:
                return CONTROLLER_ID_MIME_TYPE;
			default:
		}
		return null;
	}

	@Override
	public Uri insert ( Uri uri, ContentValues cv ) {
		SQLiteDatabase db = data.getWritableDatabase();
		long id = 0;
		String path = null;
		switch ( sUriMatcher.match( uri ) ) {
			case CODE_STATUS:
				id = db.insert( StatusTable.TABLE_NAME, null, cv );
				path = PATH_STATUS;
				break;
			case CODE_ERROR:
				id = db.insert( ErrorTable.TABLE_NAME, null, cv );
				path = PATH_ERROR;
				break;
			case CODE_NOTIFICATION:
				id = db.insert( NotificationTable.TABLE_NAME, null, cv );
				path = PATH_NOTIFICATION;
				break;
			case CODE_USER_MEMORY:
				id = db.insert( UserMemoryLocationsTable.TABLE_NAME, null, cv );
				path = PATH_USER_MEMORY;
				break;
            case CODE_CONTROLLER:
                // TODO verify insertion of Controller into ControllersTable works
                id = db.insert( ControllersTable.TABLE_NAME, null, cv );
                path = PATH_CONTROLLER;
                break;
			default:
				throw new IllegalArgumentException( "Unknown URI: " + uri );
		}
		getContext().getContentResolver().notifyChange( uri, null );
		return Uri.parse( path + "/" + id );
	}

	@Override
	public int delete ( Uri uri, String selection, String[] selectionArgs ) {
		int rowsDeleted = 0;
		SQLiteDatabase db = data.getWritableDatabase();
		switch ( sUriMatcher.match( uri ) ) {
			case CODE_STATUS:
				rowsDeleted =
						db.delete(	StatusTable.TABLE_NAME, selection,
									selectionArgs );
				break;
			case CODE_STATUS_ID:
				rowsDeleted =
						db.delete(	StatusTable.TABLE_NAME, StatusTable.COL_ID
															+ "=?",
									new String[] { uri.getLastPathSegment() } );
				break;
			case CODE_ERROR:
				rowsDeleted =
						db.delete(	ErrorTable.TABLE_NAME, selection,
									selectionArgs );
				break;
			case CODE_ERROR_ID:
				rowsDeleted =
						db.delete(	ErrorTable.TABLE_NAME, ErrorTable.COL_ID
															+ "=?",
									new String[] { uri.getLastPathSegment() } );
				break;
			case CODE_NOTIFICATION:
				rowsDeleted =
						db.delete(	NotificationTable.TABLE_NAME, selection,
									selectionArgs );
				break;
			case CODE_NOTIFICATION_ID:
				rowsDeleted =
						db.delete(	NotificationTable.TABLE_NAME,
									NotificationTable.COL_ID + "=?",
									new String[] { uri.getLastPathSegment() } );
				break;
			case CODE_USER_MEMORY:
				rowsDeleted = db.delete(UserMemoryLocationsTable.TABLE_NAME, selection, selectionArgs);
				break;
			case CODE_USER_MEMORY_ID:
				rowsDeleted = db.delete(UserMemoryLocationsTable.TABLE_NAME,
						UserMemoryLocationsTable.COL_ID + "=?",
						new String[] {uri.getLastPathSegment()});
				break;
            case CODE_CONTROLLER_ID:
                // TODO verify deletion of Controller from ControllersTable works
                rowsDeleted =
                        db.delete( ControllersTable.TABLE_NAME,
                                    ControllersTable.COL_CONTROLLER_ID + "=?",
                                    new String[] { uri.getLastPathSegment() } );
                /* TODO need to delete all references in other tables to the deleted controller
                When deleting a controller, we need to delete all of the following:
                    * errors
                    * notifications
                    * status
                    * labels
                    * enabled ports
                    * probes visibility
                 related to the deleted controller
                 */
                break;
			default:
				throw new IllegalArgumentException( "Unknown URI: " + uri );
		}
		getContext().getContentResolver().notifyChange( uri, null );
		return rowsDeleted;
	}

	public int update (
			Uri uri,
			ContentValues values,
			String selection,
			String[] selectionArgs ) {
		int rowsUpdated = 0;
		SQLiteDatabase db = data.getWritableDatabase();
		switch ( sUriMatcher.match( uri ) ) {
			case CODE_ERROR:
				rowsUpdated =
						db.update(	ErrorTable.TABLE_NAME, values, selection,
									selectionArgs );
				break;
			case CODE_NOTIFICATION:
				rowsUpdated =
						db.update(	NotificationTable.TABLE_NAME, values,
									selection, selectionArgs );
				break;
			case CODE_USER_MEMORY:
				rowsUpdated = db.update(UserMemoryLocationsTable.TABLE_NAME, values, selection, selectionArgs);
				break;
            case CODE_CONTROLLER:
                // TODO verify updating of Controller in ControllersTable works
                rowsUpdated =
                        db.update( ControllersTable.TABLE_NAME, values,
                                    selection, selectionArgs);
                break;
			case CODE_STATUS_ID:
				rowsUpdated =
						db.update( StatusTable.TABLE_NAME, values,
								StatusTable.COL_ID + "=?",
								new String[] { uri.getLastPathSegment() } );
				break;
			default:
				throw new IllegalArgumentException( "Unknown Update URI: "
													+ uri );
		}
		getContext().getContentResolver().notifyChange( uri, null );
		return rowsUpdated;
	}

} public class StatusTable {

	//private static final String TAG = StatusTable.class.getSimpleName();

	// Database constants
	public static final String TABLE_NAME = "params";
	// columns
	public static final String COL_ID = "_id";
	public static final String COL_T1 = "t1";
	public static final String COL_T2 = "t2";
	public static final String COL_T3 = "t3";
	public static final String COL_PH = "ph";
	public static final String COL_DP = "dp";
	public static final String COL_AP = "ap";
	public static final String COL_ATOHI = "atohi";
	public static final String COL_ATOLO = "atolow";
	public static final String COL_SAL = "sal";
	public static final String COL_ORP = "orp";
	public static final String COL_LOGDATE = "logdate";
	public static final String COL_RDATA = "rdata";
	public static final String COL_RONMASK = "ronmask";
	public static final String COL_ROFFMASK = "roffmask";
	public static final String COL_R1DATA = "r1data";
	public static final String COL_R1ONMASK = "r1onmask";
	public static final String COL_R1OFFMASK = "r1offmask";
	public static final String COL_R2DATA = "r2data";
	public static final String COL_R2ONMASK = "r2onmask";
	public static final String COL_R2OFFMASK = "r2offmask";
	public static final String COL_R3DATA = "r3data";
	public static final String COL_R3ONMASK = "r3onmask";
	public static final String COL_R3OFFMASK = "r3offmask";
	public static final String COL_R4DATA = "r4data";
	public static final String COL_R4ONMASK = "r4onmask";
	public static final String COL_R4OFFMASK = "r4offmask";
	public static final String COL_R5DATA = "r5data";
	public static final String COL_R5ONMASK = "r5onmask";
	public static final String COL_R5OFFMASK = "r5offmask";
	public static final String COL_R6DATA = "r6data";
	public static final String COL_R6ONMASK = "r6onmask";
	public static final String COL_R6OFFMASK = "r6offmask";
	public static final String COL_R7DATA = "r7data";
	public static final String COL_R7ONMASK = "r7onmask";
	public static final String COL_R7OFFMASK = "r7offmask";
	public static final String COL_R8DATA = "r8data";
	public static final String COL_R8ONMASK = "r8onmask";
	public static final String COL_R8OFFMASK = "r8offmask";
	public static final String COL_PWME0 = "pwme0";
	public static final String COL_PWME1 = "pwme1";
	public static final String COL_PWME2 = "pwme2";
	public static final String COL_PWME3 = "pwme3";
	public static final String COL_PWME4 = "pwme4";
	public static final String COL_PWME5 = "pwme5";
	public static final String COL_AIW = "aiw";
	public static final String COL_AIB = "aib";
	public static final String COL_AIRB = "airb";
	public static final String COL_RFM = "rfm";
	public static final String COL_RFS = "rfs";
	public static final String COL_RFD = "rfd";
	public static final String COL_RFW = "rfw";
	public static final String COL_RFRB = "rfrb";
	public static final String COL_RFR = "rfr";
	public static final String COL_RFG = "rfg";
	public static final String COL_RFB = "rfb";
	public static final String COL_RFI = "rfi";
	public static final String COL_IO = "io";
	public static final String COL_C0 = "c0";
	public static final String COL_C1 = "c1";
	public static final String COL_C2 = "c2";
	public static final String COL_C3 = "c3";
	public static final String COL_C4 = "c4";
	public static final String COL_C5 = "c5";
	public static final String COL_C6 = "c6";
	public static final String COL_C7 = "c7";
	public static final String COL_EM = "em";
	public static final String COL_REM = "rem";
	public static final String COL_PHE = "phe";
	public static final String COL_WL = "wl";
    public static final String COL_WL1 = "wl1";
    public static final String COL_WL2 = "wl2";
    public static final String COL_WL3 = "wl3";
    public static final String COL_WL4 = "wl4";
    public static final String COL_EM1 = "em1";
    public static final String COL_HUM = "hum";
    public static final String COL_PWMAO = "pwmao";
    public static final String COL_PWMDO = "pwmdo";
    public static final String COL_PWME0O = "pwme0o";
    public static final String COL_PWME1O = "pwme1o";
    public static final String COL_PWME2O = "pwme2o";
    public static final String COL_PWME3O = "pwme3o";
    public static final String COL_PWME4O = "pwme4o";
    public static final String COL_PWME5O = "pwme5o";
    public static final String COL_AIWO = "aiwo";
    public static final String COL_AIBO = "aibo";
    public static final String COL_AIRBO = "airbo";
    public static final String COL_RFWO = "rfwo";
    public static final String COL_RFRBO = "rfrbo";
    public static final String COL_RFRO = "rfro";
    public static final String COL_RFGO = "rfgo";
    public static final String COL_RFBO = "rfbo";
    public static final String COL_RFIO = "rfio";
    public static final String COL_AF = "af";
    public static final String COL_SF = "sf";
    public static final String COL_SCPWME0 = "scpwme0";
    public static final String COL_SCPWME0O = "scpwme0o";
    public static final String COL_SCPWME1 = "scpwme1";
    public static final String COL_SCPWME1O = "scpwme1o";
    public static final String COL_SCPWME2 = "scpwme2";
    public static final String COL_SCPWME2O = "scpwme2o";
    public static final String COL_SCPWME3 = "scpwme3";
    public static final String COL_SCPWME3O = "scpwme3o";
    public static final String COL_SCPWME4 = "scpwme4";
    public static final String COL_SCPWME4O = "scpwme4o";
    public static final String COL_SCPWME5 = "scpwme5";
    public static final String COL_SCPWME5O = "scpwme5o";
    public static final String COL_SCPWME6 = "scpwme6";
    public static final String COL_SCPWME6O = "scpwme6o";
    public static final String COL_SCPWME7 = "scpwme7";
    public static final String COL_SCPWME7O = "scpwme7o";
    public static final String COL_SCPWME8 = "scpwme8";
    public static final String COL_SCPWME8O = "scpwme8o";
    public static final String COL_SCPWME9 = "scpwme9";
    public static final String COL_SCPWME9O = "scpwme9o";
    public static final String COL_SCPWME10 = "scpwme10";
    public static final String COL_SCPWME10O = "scpwme10o";
    public static final String COL_SCPWME11 = "scpwme11";
    public static final String COL_SCPWME11O = "scpwme11o";
    public static final String COL_SCPWME12 = "scpwme12";
    public static final String COL_SCPWME12O = "scpwme12o";
    public static final String COL_SCPWME13 = "scpwme13";
    public static final String COL_SCPWME13O = "scpwme13o";
    public static final String COL_SCPWME14 = "scpwme14";
    public static final String COL_SCPWME14O = "scpwme14o";
    public static final String COL_SCPWME15 = "scpwme15";
    public static final String COL_SCPWME15O = "scpwme15o";
    public static final String COL_DCM = "dcm";
    public static final String COL_DCS = "dcs";
    public static final String COL_DCD = "dcd";
    public static final String COL_DCT = "dct";
    public static final String COL_PWMD2 = "pwmd2";
    public static final String COL_PWMD2O = "pwmd2o";
    public static final String COL_PWMA2 = "pwma2";
    public static final String COL_PWMA2O = "pwma2o";
    public static final String COL_BOARD = "bid";
    public static final String COL_PAR = "par";


	public static void onCreate ( SQLiteDatabase db ) {
		// create parameters table
        db.execSQL( "CREATE TABLE " + TABLE_NAME + " (" + COL_ID
                + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COL_T1
                + " TEXT, " + COL_T2 + " TEXT, " + COL_T3 + " TEXT, "
                + COL_PH + " TEXT, " + COL_DP + " INTEGER, " + COL_AP
                + " INTEGER, " + COL_SAL + " TEXT, " + COL_ORP + " TEXT, "
                + COL_ATOHI + " INTEGER, " + COL_ATOLO + " INTEGER, "
                + COL_LOGDATE + " TEXT, " + COL_RDATA + " INTEGER, "
                + COL_RONMASK + " INTEGER, " + COL_ROFFMASK + " INTEGER, "
                + COL_R1DATA + " INTEGER, " + COL_R1ONMASK + " INTEGER, "
                + COL_R1OFFMASK + " INTEGER, " + COL_R2DATA + " INTEGER, "
                + COL_R2ONMASK + " INTEGER, " + COL_R2OFFMASK
                + " INTEGER, " + COL_R3DATA + " INTEGER, " + COL_R3ONMASK
                + " INTEGER, " + COL_R3OFFMASK + " INTEGER, " + COL_R4DATA
                + " INTEGER, " + COL_R4ONMASK + " INTEGER, "
                + COL_R4OFFMASK + " INTEGER, " + COL_R5DATA + " INTEGER, "
                + COL_R5ONMASK + " INTEGER, " + COL_R5OFFMASK
                + " INTEGER, " + COL_R6DATA + " INTEGER, " + COL_R6ONMASK
                + " INTEGER, " + COL_R6OFFMASK + " INTEGER, " + COL_R7DATA
                + " INTEGER, " + COL_R7ONMASK + " INTEGER, "
                + COL_R7OFFMASK + " INTEGER, " + COL_R8DATA + " INTEGER, "
                + COL_R8ONMASK + " INTEGER, " + COL_R8OFFMASK
                + " INTEGER, " + COL_PWME0 + " INTEGER, " + COL_PWME1
                + " INTEGER, " + COL_PWME2 + " INTEGER, " + COL_PWME3
                + " INTEGER, " + COL_PWME4 + " INTEGER, " + COL_PWME5
                + " INTEGER, " + COL_AIW + " INTEGER, " + COL_AIB
                + " INTEGER, " + COL_AIRB + " INTEGER, " + COL_RFM
                + " INTEGER, " + COL_RFS + " INTEGER, " + COL_RFD
                + " INTEGER, " + COL_RFW + " INTEGER, " + COL_RFRB
                + " INTEGER, " + COL_RFR + " INTEGER, " + COL_RFG
                + " INTEGER, " + COL_RFB + " INTEGER, " + COL_RFI
                + " INTEGER, " + COL_IO + " INTEGER, " + COL_C0
                + " INTEGER, " + COL_C1 + " INTEGER, " + COL_C2
                + " INTEGER, " + COL_C3 + " INTEGER, " + COL_C4
                + " INTEGER, " + COL_C5 + " INTEGER, " + COL_C6
                + " INTEGER, " + COL_C7 + " INTEGER, " + COL_EM
                + " INTEGER, " + COL_REM + " INTEGER, " + COL_PHE
                + " TEXT, " + COL_WL + " INTEGER, " + COL_WL1
                + " INTEGER, " + COL_WL2 + " INTEGER, " + COL_WL3
                + " INTEGER, " + COL_WL4 + " INTEGER, " + COL_EM1
                + " INTEGER, " + COL_HUM + " INTEGER, "
                + COL_PWMAO + " INTEGER DEFAULT 255, "
                + COL_PWMDO + " INTEGER DEFAULT 255, "
                + COL_PWME0O + " INTEGER DEFAULT 255, "
                + COL_PWME1O + " INTEGER DEFAULT 255, "
                + COL_PWME2O + " INTEGER DEFAULT 255, "
                + COL_PWME3O + " INTEGER DEFAULT 255, "
                + COL_PWME4O + " INTEGER DEFAULT 255, "
                + COL_PWME5O + " INTEGER DEFAULT 255, "
                + COL_AIWO + " INTEGER DEFAULT 255, "
                + COL_AIBO + " INTEGER DEFAULT 255, "
                + COL_AIRBO + " INTEGER DEFAULT 255, "
                + COL_RFWO + " INTEGER DEFAULT 255, "
                + COL_RFRBO + " INTEGER DEFAULT 255, "
                + COL_RFRO + " INTEGER DEFAULT 255, "
                + COL_RFGO + " INTEGER DEFAULT 255, "
                + COL_RFBO + " INTEGER DEFAULT 255, "
                + COL_RFIO + " INTEGER DEFAULT 255, "
                + COL_AF + " INTEGER DEFAULT 0, "
                + COL_SF + " INTEGER DEFAULT 0, "
                + COL_SCPWME0 + " INTEGER, "
                + COL_SCPWME0O + " INTEGER DEFAULT 255, "
                + COL_SCPWME1 + " INTEGER, "
                + COL_SCPWME1O + " INTEGER DEFAULT 255, "
                + COL_SCPWME2 + " INTEGER, "
                + COL_SCPWME2O + " INTEGER DEFAULT 255, "
                + COL_SCPWME3 + " INTEGER, "
                + COL_SCPWME3O + " INTEGER DEFAULT 255, "
                + COL_SCPWME4 + " INTEGER, "
                + COL_SCPWME4O + " INTEGER DEFAULT 255, "
                + COL_SCPWME5 + " INTEGER, "
                + COL_SCPWME5O + " INTEGER DEFAULT 255, "
                + COL_SCPWME6 + " INTEGER, "
                + COL_SCPWME6O + " INTEGER DEFAULT 255, "
                + COL_SCPWME7 + " INTEGER, "
                + COL_SCPWME7O + " INTEGER DEFAULT 255, "
                + COL_SCPWME8 + " INTEGER, "
                + COL_SCPWME8O + " INTEGER DEFAULT 255, "
                + COL_SCPWME9 + " INTEGER, "
                + COL_SCPWME9O + " INTEGER DEFAULT 255, "
                + COL_SCPWME10 + " INTEGER, "
                + COL_SCPWME10O + " INTEGER DEFAULT 255, "
                + COL_SCPWME11 + " INTEGER, "
                + COL_SCPWME11O + " INTEGER DEFAULT 255, "
                + COL_SCPWME12 + " INTEGER, "
                + COL_SCPWME12O + " INTEGER DEFAULT 255, "
                + COL_SCPWME13 + " INTEGER, "
                + COL_SCPWME13O + " INTEGER DEFAULT 255, "
                + COL_SCPWME14 + " INTEGER, "
                + COL_SCPWME14O + " INTEGER DEFAULT 255, "
                + COL_SCPWME15 + " INTEGER, "
                + COL_SCPWME15O + " INTEGER DEFAULT 255, "
                + COL_DCM + " INTEGER, "
                + COL_DCS + " INTEGER, "
                + COL_DCD + " INTEGER, "
                + COL_DCT + " INTEGER, "
                + COL_PWMA2 + " INTEGER, "
                + COL_PWMD2 + " INTEGER, "
                + COL_PWMA2O + " INTEGER DEFAULT 255, "
                + COL_PWMD2O + " INTEGER DEFAULT 255, "
                + COL_PAR + " INTEGER, "
                + COL_BOARD + " INTEGER DEFAULT 1"

                + ");" );

	}

	public static void onUpgrade (
			SQLiteDatabase db,
			int oldVersion,
			int newVersion ) {
        int curVer = oldVersion;
        while ( curVer < newVersion ) {
            curVer++;
            // only list the versions that there were changes made
            switch ( curVer ) {
                default:
                    break;
                case 4:
                    upgradeToVersion4(db);
                    break;
                case 7:
                    upgradeToVersion7(db);
                    break;
                case 8:
                    upgradeToVersion8(db);
                    break;
                case 9:
                    upgradeToVersion9(db);
                    break;
                case 10:
                    upgradeToVersion10(db);
                    break;
                case 11:
                    upgradeToVersion11(db);
                    break;
                case 12:
                    upgradeToVersion12(db);
                    break;
                case 13:
                    upgradeToVersion13(db);
                    break;
            }
        }
	}

    // no need to worry about having extra columns in the status table on downgrading
//	public static void onDowngrade ( SQLiteDatabase db,
//			int oldVersion,
//			int newVersion ) {
//		int curVer = oldVersion;
//		while ( curVer > newVersion ) {
//			curVer--;
//			switch ( curVer ) {
//				default:
//					break;
//				case 6:
//					downgradeToVersion6(db);
//					break;
//			}
//		}
//	}

    private static void upgradeToVersion4(SQLiteDatabase db) {
        // clear everything and drop the table
        db.execSQL( "DROP TABLE IF EXISTS " + TABLE_NAME );
        onCreate( db );
    }

    private static void upgradeToVersion7(SQLiteDatabase db) {
        // added in additional water level columns
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_WL1 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_WL2 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_WL3 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_WL4 + " INTEGER;" );
    }

    private static void upgradeToVersion8(SQLiteDatabase db) {
        // added in EM1 column
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_EM1 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_HUM + " INTEGER;" );
    }

    private static void upgradeToVersion9(SQLiteDatabase db) {
        // add in pwm override channels
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWMAO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWMDO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWME0O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWME1O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWME2O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWME3O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWME4O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWME5O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_AIWO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_AIBO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_AIRBO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_RFWO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_RFRBO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_RFRO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_RFGO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_RFBO + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_RFIO + " INTEGER DEFAULT 255;" );
    }

    private static void upgradeToVersion10(SQLiteDatabase db) {
        // add in alert and status flags fields
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_AF + " INTEGER DEFAULT 0;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SF + " INTEGER DEFAULT 0;" );
    }

    private static void upgradeToVersion11(SQLiteDatabase db) {
        // add in 16 channel pwm support
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME0 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME0O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME1 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME1O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME2 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME2O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME3 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME3O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME4 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME4O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME5 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME5O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME6 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME6O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME7 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME7O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME8 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME8O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME9 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME9O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME10 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME10O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME11 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME11O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME12 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME12O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME13 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME13O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME14 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME14O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME15 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_SCPWME15O + " INTEGER DEFAULT 255;" );
    }

    private static void upgradeToVersion12(SQLiteDatabase db) {
        // add in dc pump support
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_DCM + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_DCS + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_DCD + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_DCT + " INTEGER;" );
    }

    private static void upgradeToVersion13(SQLiteDatabase db) {
        // add in extra columns for ra* support plus the par expansion module
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWMA2 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWMD2 + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWMA2O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PWMD2O + " INTEGER DEFAULT 255;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_PAR + " INTEGER;" );
        db.execSQL( "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COL_BOARD + " INTEGER DEFAULT 1;" );
    }
} public class AlarmReceiver extends BroadcastReceiver {

    public static final String ALARM_TYPE = "alarm_type";
    public static final int UPDATE = 10;
    public static final int NOTIFICATION = 20;

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d("AlarmReceiver", "onReceive");
        int type = intent.getIntExtra(ALARM_TYPE, UPDATE);
        if ( type == NOTIFICATION ) {
            Log.d("AlarmReceiver", "NotificationService");
            intent.setClass(context, NotificationService.class);
            NotificationService.enqueueWork(context, intent);
        } else {
            Log.d("AlarmReceiver", "UpdateService");
            intent.setClass(context, UpdateService.class);
            UpdateService.enqueueWork(context, intent);
        }
    }
} public final class Globals {
    public static final String PACKAGE = buildPackage();
    public static final String PRE10_LOCATIONS = "PreLocations";
    public static final String RELAY_BOX_NUMBER = "RelayBoxNumber";
    public static final String loggingFile = "ra_log.txt";
    public static final String dateConversionFile = "ra_convert_dates.txt";
    public static final int logReplace = 0;
    public static final int logAppend = 1;
    public static final int errorRetryNone = 0;
    public static final int memoryReadOnly = -1;
    public static final int defaultPort = 9;

    // profile updating
    public static final int profileAlways = 0;
    public static final int profileOnlyAway = 1;
    public static final int profileOnlyHome = 2;
    public static final int profileHome = 0;
    public static final int profileAway = 1;

    // notification conditions
    public static final int condGreaterThan = 0;
    public static final int condGreaterThanOrEqualTo = 1;
    public static final int condEqual = 2;
    public static final int condLessThan = 3;
    public static final int condLessThanOrEqualTo = 4;
    public static final int condNotEqual = 5;

    // notification parameters
    public static final int paramT1 = 0;
    public static final int paramT2 = 1;
    public static final int paramT3 = 2;
    public static final int paramPH = 3;
    public static final int paramPHExpansion = 4;
    public static final int paramDaylightPWM = 5;
    public static final int paramActinicPWM = 6;
    public static final int paramSalinity = 7;
    public static final int paramORP = 8;
    public static final int paramWaterLevel = 9;
    public static final int paramATOHigh = 10;
    public static final int paramATOLow = 11;
    public static final int paramPWMExp0 = 12;
    public static final int paramPWMExp1 = 13;
    public static final int paramPWMExp2 = 14;
    public static final int paramPWMExp3 = 15;
    public static final int paramPWMExp4 = 16;
    public static final int paramPWMExp5 = 17;
    public static final int paramAIWhite = 18;
    public static final int paramAIBlue = 19;
    public static final int paramAIRoyalBlue = 20;
    public static final int paramVortechMode = 21;
    public static final int paramVortechSpeed = 22;
    public static final int paramVortechDuration = 23;
    public static final int paramRadionWhite = 24;
    public static final int paramRadionRoyalBlue = 25;
    public static final int paramRadionRed = 26;
    public static final int paramRadionGreen = 27;
    public static final int paramRadionBlue = 28;
    public static final int paramRadionIntensity = 29;
    public static final int paramIOCh0 = 30;
    public static final int paramIOCh1 = 31;
    public static final int paramIOCh2 = 32;
    public static final int paramIOCh3 = 33;
    public static final int paramIOCh4 = 34;
    public static final int paramIOCh5 = 35;
    public static final int paramCustom0 = 36;
    public static final int paramCustom1 = 37;
    public static final int paramCustom2 = 38;
    public static final int paramCustom3 = 39;
    public static final int paramCustom4 = 40;
    public static final int paramCustom5 = 41;
    public static final int paramCustom6 = 42;
    public static final int paramCustom7 = 43;
    public static final int paramWaterLevel1 = 44;
    public static final int paramWaterLevel2 = 45;
    public static final int paramWaterLevel3 = 46;
    public static final int paramWaterLevel4 = 47;
    public static final int paramHumidity = 48;
    public static final int paramSCPWMExp0 = 49;
    public static final int paramSCPWMExp1 = 50;
    public static final int paramSCPWMExp2 = 51;
    public static final int paramSCPWMExp3 = 52;
    public static final int paramSCPWMExp4 = 53;
    public static final int paramSCPWMExp5 = 54;
    public static final int paramSCPWMExp6 = 55;
    public static final int paramSCPWMExp7 = 56;
    public static final int paramSCPWMExp8 = 57;
    public static final int paramSCPWMExp9 = 58;
    public static final int paramSCPWMExp10 = 59;
    public static final int paramSCPWMExp11 = 60;
    public static final int paramSCPWMExp12 = 61;
    public static final int paramSCPWMExp13 = 62;
    public static final int paramSCPWMExp14 = 63;
    public static final int paramSCPWMExp15 = 64;
    public static final int paramDaylight2PWM = 65;
    public static final int paramActinic2PWM = 66;
    public static final int paramAlarm = 67;
    public static final int paramPAR = 68;

    // override locations
    public static final int OVERRIDE_DISABLE = 255;
    public static final int OVERRIDE_MAX_VALUE = 100;
    public static final int OVERRIDE_DAYLIGHT = 0;
    public static final int OVERRIDE_ACTINIC = 1;
    public static final int OVERRIDE_CHANNEL0 = 2;
    public static final int OVERRIDE_CHANNEL1 = 3;
    public static final int OVERRIDE_CHANNEL2 = 4;
    public static final int OVERRIDE_CHANNEL3 = 5;
    public static final int OVERRIDE_CHANNEL4 = 6;
    public static final int OVERRIDE_CHANNEL5 = 7;
    public static final int OVERRIDE_AI_WHITE = 8;
    public static final int OVERRIDE_AI_BLUE = 9;
    public static final int OVERRIDE_AI_ROYALBLUE = 10;
    public static final int OVERRIDE_RF_WHITE = 11;
    public static final int OVERRIDE_RF_ROYALBLUE = 12;
    public static final int OVERRIDE_RF_RED = 13;
    public static final int OVERRIDE_RF_GREEN = 14;
    public static final int OVERRIDE_RF_BLUE = 15;
    public static final int OVERRIDE_RF_INTENSITY = 16;
    public static final int OVERRIDE_DAYLIGHT2 = 17;
    public static final int OVERRIDE_ACTINIC2 = 18;
    public static final int OVERRIDE_16CH_CHANNEL0 = 19;
    public static final int OVERRIDE_16CH_CHANNEL1 = 20;
    public static final int OVERRIDE_16CH_CHANNEL2 = 21;
    public static final int OVERRIDE_16CH_CHANNEL3 = 22;
    public static final int OVERRIDE_16CH_CHANNEL4 = 23;
    public static final int OVERRIDE_16CH_CHANNEL5 = 24;
    public static final int OVERRIDE_16CH_CHANNEL6 = 25;
    public static final int OVERRIDE_16CH_CHANNEL7 = 26;
    public static final int OVERRIDE_16CH_CHANNEL8 = 27;
    public static final int OVERRIDE_16CH_CHANNEL9 = 28;
    public static final int OVERRIDE_16CH_CHANNEL10 = 29;
    public static final int OVERRIDE_16CH_CHANNEL11 = 30;
    public static final int OVERRIDE_16CH_CHANNEL12 = 31;
    public static final int OVERRIDE_16CH_CHANNEL13 = 32;
    public static final int OVERRIDE_16CH_CHANNEL14 = 33;
    public static final int OVERRIDE_16CH_CHANNEL15 = 34;
    //public static final int OVERRIDE_CHANNELS = 35;

    // calibrate locations
    public static final int CALIBRATE_PH = 0;
    public static final int CALIBRATE_SALINITY = 1;
    public static final int CALIBRATE_ORP = 2;
    public static final int CALIBRATE_PHE = 3;
    public static final int CALIBRATE_WATERLEVEL = 4;

    // Controller Indices
    public static final int T1_INDEX = 0;
    public static final int T2_INDEX = 1;
    public static final int T3_INDEX = 2;
    public static final int PH_INDEX = 3;
    public static final int DP_INDEX = 4;
    public static final int AP_INDEX = 5;
    public static final int DP2_INDEX = 6;
    public static final int AP2_INDEX = 7;
    public static final int ATOLO_INDEX = 8;
    public static final int ATOHI_INDEX = 9;
    public static final int SALINITY_INDEX = 10;
    public static final int ORP_INDEX = 11;
    public static final int PHE_INDEX = 12;
    public static final int WL_INDEX = 13;
    public static final int WL1_INDEX = 14;
    public static final int WL2_INDEX = 15;
    public static final int WL3_INDEX = 16;
    public static final int WL4_INDEX = 17;
    public static final int HUMIDITY_INDEX = 18;
    public static final int PAR_INDEX = 19;

    // variable ranges
    public static final int BYTE_MIN = 0;
    public static final int BYTE_MAX = 255;
    public static final int INT_MIN = 0;
    public static final int INT_MAX = 32767;


    private static String buildPackage() {
        String p = Globals.class.getPackage().getName();
        if (BuildConfig.DEBUG) {
            p += ".debug";
        }
        return p;
    }
} public final class Permissions {
    public static final String PERMISSION_BASE =
            "info.curtbinder.reefangel.permission";

    /*
     * QUERY_STATUS permission handles sending / receiving the following
     * messages
     *
     * QUERY_STATUS UPDATE_STATUS UPDATE_DISPLAY_DATA ERROR_MESSAGE
     */
    public static final String QUERY_STATUS = PERMISSION_BASE + ".QUERY_STATUS";
    /*
     * SEND_COMMAND permission handles sending / receiving the following
     * messages
     *
     * COMMAND_SEND LABEL_QUERY MEMORY_SEND TOGGLE_RELAY VERSION_QUERY
     *
     * COMMAND_RESPONSE LABEL_RESPONSE MEMORY_RESPONSE VERSION_RESPONSE
     */
    public static final String SEND_COMMAND = PERMISSION_BASE + ".SEND_COMMAND";
} public class RAApplication extends Application {

    private static final String TAG = RAApplication.class.getSimpleName();
    private static final String NUMBER_PATTERN = "\\d+";
    private static final String HOST_PATTERN =
            "^(?i:[[0-9][a-z]]+)(?i:[\\w\\.\\-]*)(?i:[[0-9][a-z]]+)$";
    private static final String USERID_PATTERN = "[\\w\\-\\.\\x20]+";
    @SuppressWarnings("unused")
    private static final String WIFI_LOOKUP =
            "http://forum.reefangel.com/getwifi.php?id=%1$s&pwd=%2$s";

    // Preferences
    public RAPreferences raprefs;

    // Error code stuff
    private String[] errorCodes;
    private String[] errorCodesStrings;
    private String errorCodeMessage;
    public int errorCode;
    public int errorCount;

    public void onCreate() {
        super.onCreate();
        errorCodes = getResources().getStringArray(R.array.errorCodes);
        errorCodesStrings = getResources().getStringArray(R.array.errorCodesStrings);
        errorCodeMessage = ""; // set to no error message
        errorCode = 0; // set to no error initially
        raprefs = new RAPreferences(this);

        // initialize the error count
        errorCount = 0;

        // create the notification channels for Android O and later
        createNotificationChannels();
    }

    public void onTerminate() {
        super.onTerminate();
    }

    public void restartAutoUpdateService() {
        cancelAutoUpdateService();
        startAutoUpdateService();
    }

    public void cancelAutoUpdateService() {
        PendingIntent pi = getUpdateIntent();
        AlarmManager am =
                (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        // cancel the repeating service
        am.cancel(pi);
    }

    public void startAutoUpdateService() {
        // check to see if we need to start the repeating update service
        // grab the service interval, make sure it's greater than 0
        long interval = raprefs.getUpdateInterval();
        if (interval == 0) {
            Log.d(TAG, "disabled autoupdate");
            return;
        }

        int up = raprefs.getUpdateProfile();
        if (raprefs.isCommunicateController()) {
            int p = getSelectedProfile();
            Log.d(TAG, "UP: " + up + " P: " + p);
            if (isAwayProfileEnabled()) {
                if ((up == Globals.profileOnlyAway)
                        && (p != Globals.profileAway)) {
                    // only run on away profile and we are not on away profile
                    Log.d(TAG, "only run on away, not away");
                    return;
                } else if ((up == Globals.profileOnlyHome)
                        && (p != Globals.profileHome)) {
                    // only run on home profile and we are not on home profile
                    Log.d(TAG, "only run on home, not home");
                    return;
                }
            }
        }

        // create a status query message
        PendingIntent pi = getUpdateIntent();
        // setup alarm service to wake up and start the service periodically
        AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        am.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), interval, pi);
    }

    private PendingIntent getUpdateIntent() {
        Intent i = new Intent(this, AlarmReceiver.class);
        i.setAction(MessageCommands.QUERY_STATUS_INTENT);
        i.putExtra(AlarmReceiver.ALARM_TYPE, AlarmReceiver.UPDATE);
        i.putExtra(MessageCommands.AUTO_UPDATE_PROFILE_INT,
                raprefs.getUpdateProfile());
        return PendingIntent.getBroadcast(this, -1, i,
                PendingIntent.FLAG_CANCEL_CURRENT);
    }

    public void createNotificationChannels() {
        // This is for Android O and later
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // Setup the channels
            // 2 channels:  Notifications and Errors
            CharSequence ch1_name = getString(R.string.menuMainNotifications);
            String ch1_desc = getString(R.string.descriptionNotificationChannel);
            NotificationChannel ch1 = new NotificationChannel(NotificationService.NOTIFICATION_CHANNEL_ID,
                    ch1_name, NotificationManager.IMPORTANCE_HIGH);
            ch1.setDescription(ch1_desc);
//            CharSequence ch2_name = getString(R.string.menuMainErrors);
//            String ch2_desc = getString(R.string.descriptionErrorChannel);
//            NotificationChannel ch2 = new NotificationChannel(NotificationService.ERROR_CHANNEL_ID,
//                    ch1_name, NotificationManager.IMPORTANCE_DEFAULT);
//            ch2.setDescription(ch2_desc);

            NotificationManager nm =  getSystemService(NotificationManager.class);
            nm.createNotificationChannel(ch1);
//            nm.createNotificationChannel(ch2);
        }
    }

    // Error Logging
    public void clearErrorCode() {
        errorCode = 0;
        errorCodeMessage = "";
    }

    private String getSimpleErrorMessage ( String msg ) {
        String s = "";
        if ( msg.contains( "EHOSTUNREACH" ) ) {
            s = "Host unreachable: " + raprefs.getHost() + ":" + raprefs.getPort();
        } else if ( msg.contains( "ECONNREFUSED" ) ) {
            s = "Connection Refused: " + raprefs.getHost() + ":" + raprefs.getPort();
        } else if ( msg.contains( "ECONNRESET" ) ) {
            s = "Connection Reset by Peer";
        } else {
            s = msg;
        }
        return s;
    }

    public void error(int errorCodeIndex, Throwable t, String msg) {
        errorCode = Integer.parseInt(errorCodes[errorCodeIndex]);
        if ( t.getMessage() != null )
            errorCodeMessage = getSimpleErrorMessage(t.getMessage());
        if ( errorCode == 15 )
            // timeout error
            errorCodeMessage = String.format( Locale.getDefault(),
                    getString(R.string.messageErrorTimeout),
                    raprefs.getHost(), raprefs.getPort());
        Log.d(TAG, "Error: " + errorCode + ", " + errorCodeMessage);

        // if logging enabled, save the log
        if (raprefs.isLoggingEnabled()) {
            if (!hasExternalStorage()) {
                // doesn't have external storage
                Toast.makeText(this,
                        getString(R.string.messageNoExternalStorage),
                        Toast.LENGTH_LONG).show();
                return;
            }
            boolean keepFile = raprefs.isLoggingAppendFile();
            try {
                String sFile = getLoggingFile();
                FileWriter fw = new FileWriter(sFile, keepFile);
                PrintWriter pw = new PrintWriter(fw);
                DateFormat dft = Utils.getOldDefaultDateFormat();
                pw.println(dft.format(Calendar.getInstance().getTime()));
                String s =
                        String.format("Profile: %s\nHost: %s:%s\nUser ID: %s",
                                (getSelectedProfile() == 1) ? "Away"
                                        : "Home",
                                raprefs.getHost(), raprefs.getPort(),
                                raprefs.getUserId()
                        );
                pw.println(s);
                pw.println(msg);
                if ( t instanceof XMLReadException) {
                    // we have an XML read exception, get the xml data if any
                    pw.println( ((XMLReadException) t).getXmlData() );
                }
                pw.println(t.toString());
                pw.println("Stack Trace:");
                pw.flush();
                t.printStackTrace(pw);
                pw.println("----");
                pw.flush();
                pw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String getErrorMessage() {
        String s = getString(R.string.messageUnknownError);
        // loop through array of error codes and match with the current code
        for (int i = 0; i < errorCodes.length; i++) {
            if (Integer.parseInt(errorCodes[i]) == errorCode) {
                // found code
                s = String.format(Locale.US, "%s %d: %s", getString(R.string.messageError),
                        errorCode,
                        (errorCodeMessage == "" ) ? errorCodesStrings[i]
                                                : errorCodeMessage);
                break;
            }
        }
        return s;
    }

    public boolean canErrorRetry() {
        boolean f = false;
        if (errorCount <= raprefs.getNotificationErrorRetryMax()) {
            f = true;
        }
        return f;
    }

    public void clearErrorRetryCount() {
        errorCount = Globals.errorRetryNone;
    }

    public void increaseErrorCount() {
        errorCount++;
    }

    public String getLoggingDirectory() {
        String s =
                "" + Environment.getExternalStorageDirectory()
                        + Environment.getDataDirectory() + "/"
                        + Globals.PACKAGE + "/";
        return s;
    }

    public String getLoggingFile() {
        return getLoggingDirectory() + Globals.loggingFile;
    }

    public boolean isLoggingFilePresent() {
        return isFilePresent(getLoggingFile());
    }

    public void deleteLoggingFile() {
        File l = new File(getLoggingFile());
        if (l != null && l.exists())
            l.delete();
    }

    public boolean hasExternalStorage() {
        boolean f = false;
        File path = new File(getLoggingDirectory());
        path.mkdirs();
        File file = new File(path, "test.txt");
        file.mkdirs();
        if (file != null) {
            if (file.exists()) {
                f = true;
                file.delete();
            }
        }
        return f;
    }

    public boolean isFilePresent(String fileName) {
        boolean f = false;
        File l = new File(fileName);
        if ((l != null) && (l.exists()))
            f = true;
        return f;
    }

    public String getDateConversionFile() {
        return getLoggingDirectory() + Globals.dateConversionFile;
    }

    private boolean isDateConversionFilePresent() {
        return isFilePresent(getDateConversionFile());
    }

    public PrintWriter openDateConversionFile() {
        if (!hasExternalStorage()) {
            Log.d(TAG, "No external storage available");
            return null;
        }
        PrintWriter pw = null;
        if (!isDateConversionFilePresent()) {
            // file not present
            Log.d(TAG, "Date Conversion file not present, creating it");
        }
        try {
            String sFile = getDateConversionFile();
            FileWriter fw = new FileWriter(sFile, true);
            pw = new PrintWriter(fw);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return pw;
    }

    public void closeDateConversionFile(PrintWriter pw) {
        if ( pw != null ) {
            pw.flush();
            pw.close();
        }
    }

    public void logRepeatMsgDateConversion(PrintWriter pw, String msg) {
        if ( pw != null ) {
            DateFormat dft = Utils.getOldDefaultDateFormat();
            pw.println(dft.format(Calendar.getInstance().getTime()));
            pw.println(msg);
        }
    }

    public void exportDatabase() {
        File path = getDatabasePath(RADbHelper.DB_NAME);
        File exportedDb = new File(getLoggingDirectory(), RADbHelper.DB_NAME + "3");
        try {
            copy(path, exportedDb);
            Toast.makeText(this, R.string.messageExportSuccessful, Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            Toast.makeText(this, R.string.messageError, Toast.LENGTH_SHORT).show();
            return;
        }
        Log.d(TAG, "Exported DB: " + exportedDb.getAbsolutePath());
    }

    public void copy(File src, File dst) throws IOException {
        FileInputStream inStream = new FileInputStream(src);
        FileOutputStream outStream = new FileOutputStream(dst);
        FileChannel inChannel = inStream.getChannel();
        FileChannel outChannel = outStream.getChannel();
        inChannel.transferTo(0, inChannel.size(), outChannel);
        inStream.close();
        outStream.close();
    }

    private boolean isNumber(Object value) {
        if ((!value.toString().equals(""))
                && (value.toString().matches(NUMBER_PATTERN))) {
            return true;
        }
        return false;
    }

    public boolean validateHost(Object host) {
        // host validation here
        String h = host.toString();

        // Hosts must:
        // - not start with 'http://'
        // - only contain: alpha, number, _, -, .
        // - end with: alpha or number

        if (!h.matches(HOST_PATTERN)) {
            // invalid host
            Toast.makeText(this,
                    this.getString(R.string.prefHostInvalidHost)
                            + ": " + host.toString(),
                    Toast.LENGTH_SHORT
            ).show();
            return false;
        }
        return true;
    }

    public boolean validatePort(Object port) {
        if (!isNumber(port)) {
            // not a number
            Toast.makeText(this,
                    getString(R.string.messageNotNumber) + ": "
                            + port.toString(), Toast.LENGTH_SHORT
            )
                    .show();
            return false;
        } else {
            // it's a number, verify it's within range
            // TODO: convert min & max ports to int value defines
            int min = Integer.parseInt(getString(R.string.prefPortMin));
            int max = Integer.parseInt(getString(R.string.prefPortMax));
            int v = Integer.parseInt((String) port.toString());

            // check if it's less than the min value or if it's greater than
            // the max value
            if ((v < min) || (v > max)) {
                Toast.makeText(this,
                        getString(R.string.prefPortInvalidPort)
                                + ": " + port.toString(),
                        Toast.LENGTH_SHORT
                ).show();
                return false;
            }
        }
        return true;
    }

    public boolean validateUser(Object user) {
        String u = user.toString();
        if (!u.matches(USERID_PATTERN)) {
            // invalid userid
            Toast.makeText(this,
                    getString(R.string.prefUserIdInvalid) + ": "
                            + user.toString(), Toast.LENGTH_SHORT
            )
                    .show();
            return false;
        }
        return true;
    }

    public String getPWMOverrideChannelName(int channel) {
        String name = "";
        switch ( channel ) {
            default:
                name = getString(R.string.labelChannel);
                break;
            case Globals.OVERRIDE_DAYLIGHT:
                name = raprefs.getControllerLabel(Globals.DP_INDEX);
                break;
            case Globals.OVERRIDE_ACTINIC:
                name = raprefs.getControllerLabel(Globals.AP_INDEX);
                break;
            case Globals.OVERRIDE_CHANNEL0:
            case Globals.OVERRIDE_CHANNEL1:
            case Globals.OVERRIDE_CHANNEL2:
            case Globals.OVERRIDE_CHANNEL3:
            case Globals.OVERRIDE_CHANNEL4:
            case Globals.OVERRIDE_CHANNEL5:
                name = raprefs.getDimmingModuleChannelLabel(channel - Globals.OVERRIDE_CHANNEL0);
                break;
            case Globals.OVERRIDE_AI_WHITE:
                name = getString( R.string.labelAI ) + " " + getString( R.string.labelWhite );
                break;
            case Globals.OVERRIDE_AI_ROYALBLUE:
                name = getString( R.string.labelAI ) + " " + getString( R.string.labelRoyalBlue );
                break;
            case Globals.OVERRIDE_AI_BLUE:
                name = getString( R.string.labelAI ) + " " + getString( R.string.labelBlue );
                break;
            case Globals.OVERRIDE_RF_WHITE:
                name = getString( R.string.labelRadion ) + " " + getString( R.string.labelWhite );
                break;
            case Globals.OVERRIDE_RF_ROYALBLUE:
                name = getString( R.string.labelRadion ) + " " + getString( R.string.labelRoyalBlue );
                break;
            case Globals.OVERRIDE_RF_RED:
                name = getString( R.string.labelRadion ) + " " + getString( R.string.labelRed );
                break;
            case Globals.OVERRIDE_RF_GREEN:
                name = getString( R.string.labelRadion ) + " " + getString( R.string.labelGreen );
                break;
            case Globals.OVERRIDE_RF_BLUE:
                name = getString( R.string.labelRadion ) + " " + getString( R.string.labelBlue );
                break;
            case Globals.OVERRIDE_RF_INTENSITY:
                name = getString( R.string.labelRadion ) + " " + getString( R.string.labelIntensity );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL0:
            case Globals.OVERRIDE_16CH_CHANNEL1:
            case Globals.OVERRIDE_16CH_CHANNEL2:
            case Globals.OVERRIDE_16CH_CHANNEL3:
            case Globals.OVERRIDE_16CH_CHANNEL4:
            case Globals.OVERRIDE_16CH_CHANNEL5:
            case Globals.OVERRIDE_16CH_CHANNEL6:
            case Globals.OVERRIDE_16CH_CHANNEL7:
            case Globals.OVERRIDE_16CH_CHANNEL8:
            case Globals.OVERRIDE_16CH_CHANNEL9:
            case Globals.OVERRIDE_16CH_CHANNEL10:
            case Globals.OVERRIDE_16CH_CHANNEL11:
            case Globals.OVERRIDE_16CH_CHANNEL12:
            case Globals.OVERRIDE_16CH_CHANNEL13:
            case Globals.OVERRIDE_16CH_CHANNEL14:
            case Globals.OVERRIDE_16CH_CHANNEL15:
                name = raprefs.getSCDimmingModuleChannelLabel( channel - Globals.OVERRIDE_16CH_CHANNEL0 );
                break;
        }
        return name;
    }

    // TODO cleanup this function, remove repeated format calls, simplify
    public String getPWMOverrideMessageDisplay ( int channel ) {
        String msg = "";
        String name = getPWMOverrideChannelName(channel);
        switch ( channel ) {
            case Globals.OVERRIDE_DAYLIGHT:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefDPTitle) );
                break;
            case Globals.OVERRIDE_ACTINIC:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefAPTitle) );
                break;
            case Globals.OVERRIDE_DAYLIGHT2:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefDP2Title) );
                break;
            case Globals.OVERRIDE_ACTINIC2:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefAP2Title) );
                break;
            case Globals.OVERRIDE_CHANNEL0:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpDimmingCh0LabelTitle));
                break;
            case Globals.OVERRIDE_CHANNEL1:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpDimmingCh1LabelTitle) );
                break;
            case Globals.OVERRIDE_CHANNEL2:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpDimmingCh2LabelTitle) );
                break;
            case Globals.OVERRIDE_CHANNEL3:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpDimmingCh3LabelTitle) );
                break;
            case Globals.OVERRIDE_CHANNEL4:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpDimmingCh4LabelTitle) );
                break;
            case Globals.OVERRIDE_CHANNEL5:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpDimmingCh5LabelTitle) );
                break;
            case Globals.OVERRIDE_AI_WHITE:
            case Globals.OVERRIDE_AI_ROYALBLUE:
            case Globals.OVERRIDE_AI_BLUE:
            case Globals.OVERRIDE_RF_WHITE:
            case Globals.OVERRIDE_RF_ROYALBLUE:
            case Globals.OVERRIDE_RF_RED:
            case Globals.OVERRIDE_RF_GREEN:
            case Globals.OVERRIDE_RF_BLUE:
            case Globals.OVERRIDE_RF_INTENSITY:
                msg = name + " " + getString( R.string.labelChannel );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL0:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh0LabelTitle));
                break;
            case Globals.OVERRIDE_16CH_CHANNEL1:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh1LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL2:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh2LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL3:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh3LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL4:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh4LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL5:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh5LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL6:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh6LabelTitle));
                break;
            case Globals.OVERRIDE_16CH_CHANNEL7:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh7LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL8:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh8LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL9:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh9LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL10:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh10LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL11:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh11LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL12:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh12LabelTitle));
                break;
            case Globals.OVERRIDE_16CH_CHANNEL13:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh13LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL14:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh14LabelTitle) );
                break;
            case Globals.OVERRIDE_16CH_CHANNEL15:
                msg = String.format( Locale.getDefault(),
                        getString( R.string.messagePWMPopupCustom),
                        name, getString(R.string.prefExpSCDimmingCh15LabelTitle) );
                break;
        }
        return msg;
    }

    // Preferences
    public boolean isFirstRun() {
        // First run will be determined by:
        // if the first run key is NOT set AND
        // if the host key is NOT set OR if it's the same as the default
        boolean fFirst = raprefs.isFirstRun();

        // if it's already set, no need to compare the hosts
        if (!fFirst) {
            Log.w( TAG, "First run already set" );
            return false;
        }

        // if it's not set (as in existing installations), check the host
        // the host should be set and it should not be the same as the default
        if (!raprefs.isMainHostSet())
            return true;

        // if we have made it here, then it's an existing install where the user
        // has the host set to something other than the default
        // so we will go ahead and clear the first run prompt for them
        raprefs.disableFirstRun();
        return false;
    }

    public void displayChangeLog(AppCompatActivity a) {
        // check version code stored in preferences vs the version stored in
        // running code
        // display the changelog if the values are different
        int previous = raprefs.getPreviousCodeVersion();

        int current = 0;
        try {
            current = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
        } catch (NameNotFoundException e) {
            // safely ignore the error
        }
        if (current > previous) {
            // save code version in preferences
            raprefs.setPreviousCodeVersion(current);
            // newer version, display changelog
            DialogSupportChangelog dlg = new DialogSupportChangelog();
            dlg.show(a.getSupportFragmentManager(), "dlg");
        }
        // deletePref( R.string.prefPreviousCodeVersion );
    }

    // Profiles
    public int getSelectedProfile() {
        return raprefs.getSelectedProfile();
    }

    public void setSelectedProfile(int profile) {
        if (profile > Globals.profileAway)
            return;
        raprefs.setSelectedProfile(profile);
        restartAutoUpdateService();
    }

    public boolean isAwayProfileEnabled() {
        // String host = raprefs.getAwayHost();
        // Log.d( TAG, "isAwayProfileEnabled: " + host );
        // get away host, compare to empty host
        // if host is set, then the profile is enabled
        // if port is not set, that implies default port
        return raprefs.isAwayHostSet();
    }
} public class MainActivity extends AppCompatActivity
        implements ActionBar.OnNavigationListener {

//    public static final int REQUEST_EXIT = 1;
//    public static final int RESULT_EXIT = 1024;

    private static final String OPENED_KEY = "OPENED_KEY";
    private static final String STATE_CHECKED = "DRAWER_CHECKED";
    private static final String PREVIOUS_CHECKED = "PREVIOUS";

    // do not switch selected profile when restoring the application state
    private static boolean fRestoreState = false;
    public final String TAG = MainActivity.class.getSimpleName();
    private RAApplication raApp;
    private String[] mNavTitles;
    private Toolbar mToolbar;
    private DrawerLayout mDrawerLayout;
    private ListView mDrawerList;
    private ActionBarDrawerToggle mDrawerToggle;
    private Boolean opened = null;
    private int mOldPosition = -1;
    private Boolean fCanExit = false;
    private Fragment mHistoryContent = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        raApp = (RAApplication) getApplication();
        raApp.raprefs.setDefaultPreferences();
        // Set the Theme before the layout is instantiated
        //Utils.onActivityCreateSetTheme(this, raApp.raprefs.getSelectedTheme());

        setContentView(R.layout.activity_main);

        // Check for first run
        if (raApp.isFirstRun()) {
            Intent i = new Intent(this, SetupWizardActivity.class);
            i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
            startActivity(i);
            finish();
        }

        // Load any saved position
        int position = 0;
        if (savedInstanceState != null) {
            position = savedInstanceState.getInt(STATE_CHECKED, 0);
            Log.d(TAG, "Restore, position: " + position);
            if (position == 3) {
                // history fragment
                mHistoryContent = getSupportFragmentManager().getFragment(savedInstanceState, "HistoryGraphFragment");
            }
        }
        setupToolbar();
        setupNavDrawer();
        updateActionBar();
        selectItem(position);

        // launch a new thread to show the drawer on very first app launch
        new Thread(new Runnable() {
            @Override
            public void run() {
                opened = raApp.raprefs.getBoolean(OPENED_KEY, false);
                if (!opened) {
                    mDrawerLayout.openDrawer(mDrawerList);
                }
            }
        }).start();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // get the checked item and subtract off one to get the actual position
        // the same logic applies that is used in the DrawerItemClickedListener.onItemClicked
        int position = mDrawerList.getCheckedItemPosition() - 1;
        outState.putInt(STATE_CHECKED, position);
        if (position == 3) {
            getSupportFragmentManager().putFragment(outState, "HistoryGraphFragment", mHistoryContent);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        fCanExit = false;
        fRestoreState = true;
        setNavigationList();

        if (raApp.raprefs.isKeepScreenOnEnabled()) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }

        // last thing we do is display the changelog if necessary
        // TODO add in a preference check for displaying changelog on app startup
        raApp.displayChangeLog(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    private void setupToolbar() {
        mToolbar = (Toolbar) findViewById(R.id.toolbar);
        mToolbar.setTitle("");
        setSupportActionBar(mToolbar);
    }

    private void setupNavDrawer() {
        // get the string array for the navigation items
        mNavTitles = getResources().getStringArray(R.array.nav_items);

        // locate the navigation drawer items in the layout
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerList = (ListView) findViewById(R.id.left_drawer);

        // set a custom shadow that overlays the main content when the drawer
        // opens
        mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow,
                GravityCompat.START);

        // add in the logo header
        View header = getLayoutInflater().inflate(R.layout.drawer_list_header, null);
        mDrawerList.addHeaderView(header, null, false);

        // set the adapter for the navigation list view
        ArrayAdapter<String> adapter =
                new ArrayAdapter<String>(this, R.layout.drawer_list_item,
                        mNavTitles);
        mDrawerList.setAdapter(adapter);
        mDrawerList.setOnItemClickListener(new DrawerItemClickListener());

        // setup the toggling for the drawer
        mDrawerToggle =
                new MyDrawerToggle(this, mDrawerLayout, mToolbar,
                        R.string.drawer_open, R.string.drawer_close);
        mDrawerLayout.setDrawerListener(mDrawerToggle);
    }

    private void setNavigationList() {
        // set list navigation items
        final ActionBar ab = getSupportActionBar();
        Context context = ab.getThemedContext();
        int arrayID;
        if (raApp.isAwayProfileEnabled()) {
            arrayID = R.array.profileLabels;
        } else {
            arrayID = R.array.profileLabelsHomeOnly;
        }
        ArrayAdapter<CharSequence> list =
                ArrayAdapter.createFromResource(context, arrayID,
                        R.layout.support_simple_spinner_dropdown_item);
        ab.setListNavigationCallbacks(list, this);
        ab.setSelectedNavigationItem(raApp.getSelectedProfile());
    }

    private void updateActionBar() {
        // update actionbar
        final ActionBar ab = getSupportActionBar();
        ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
    }

    @Override
    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
        // only switch profiles when the user changes the navigation item,
        // not when the navigation list state is restored
        if (!fRestoreState) {
            raApp.setSelectedProfile(itemPosition);
        } else {
            fRestoreState = false;
        }
        return true;
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        mDrawerToggle.onConfigurationChanged(newConfig);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // sync the toggle state after onRestoreInstanceState has occurred
        mDrawerToggle.syncState();
    }

    @Override
    public void onBackPressed() {
        /*
        When the back button is pressed, this function is called.
        If the drawer is open, check it and cancel it here.

        Calling super.onBackPressed() causes the BackStackChangeListener to be called
         */
//        Log.d(TAG, "onBackPressed");
        if (mDrawerLayout.isDrawerOpen(mDrawerList)) {
//            Log.d(TAG, "drawer open, closing");
            mDrawerLayout.closeDrawer(mDrawerList);
            return;
        }
        if ( !fCanExit ) {
            Toast.makeText(this, R.string.messageExitNotification, Toast.LENGTH_SHORT).show();
            fCanExit = true;

            Handler h = new Handler();
            h.postDelayed(new Runnable() {
                @Override
                public void run() {
//                    Log.d(TAG, "Disabling exit flag");
                    fCanExit = false;
                }
            }, 2000);

            return;
        }
        super.onBackPressed();
    }

    private void updateContent(int position) {
        if (position != mOldPosition) {
            // update the main content by replacing fragments
            Fragment fragment;
            switch (position) {
                default:
                case 0:
                    fragment = StatusFragment.newInstance();
                    break;
                case 1:
                    fragment = MemoryFragment.newInstance(raApp.raprefs.useOldPre10MemoryLocations());
                    break;
                case 2:
                    fragment = NotificationsFragment.newInstance();
                    break;
                case 3:
                    //fragment = HistoryFragment.newInstance();
                    // TODO check the restoration of the fragment content
                    if (mHistoryContent != null ) {
                        fragment = mHistoryContent;
                    } else {
//                        mHistoryContent = HistoryGraphFragment.newInstance();
                        mHistoryContent = HistoryMultipleGraphFragment.newInstance();
                        fragment = mHistoryContent;
                    }
                    break;
                case 4:
                    fragment = ErrorsFragment.newInstance();
                    break;
                case 5:
                    fragment = DateTimeFragment.newInstance();
                    break;
            }

            Log.d(TAG, "UpdateContent: " + position);
            FragmentTransaction ft =
                    getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.content_frame, fragment);
            ft.commit();
            mOldPosition = position;
        }
    }

    public void selectItem(int position) {
//        Log.d(TAG, "selectItem: " + position);
        updateContent(position);
        highlightItem(position);
        mDrawerLayout.closeDrawer(mDrawerList);
    }

    public void highlightItem(int position) {
//        Log.d(TAG, "highlightItem: " + position);
        // since we are using a header for the list, the first
        // item/position in the list is the header. our header is non-selectable
        // so in order for us to have the proper item in our list selected, we must
        // increase the position by 1. this same logic is applied to the
        // DrawerItemClickedListener.onItemClicked
        mDrawerList.setItemChecked(position + 1, true);
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // pass the event to ActionBarDrawerToggle, if it returns true,
        // then it has handled the app icon touch event
        if (mDrawerToggle.onOptionsItemSelected(item)) {
            return true;
        }

        // handle the rest of the action bar items here
        switch (item.getItemId()) {
            case R.id.action_settings:
                Fragment f = getSupportFragmentManager().findFragmentById(R.id.content_frame);
                if (f instanceof StatusFragment) {
                    // the current fragment is the status fragment
                    Log.d(TAG, "Status Fragment is current");
                    ((StatusFragment) f).reloadPages();
                }
                startActivity(new Intent(this, SettingsActivity.class));
//                startActivityForResult(new Intent(this, SettingsActivity.class), REQUEST_EXIT);
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

//    @Override
//    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//        Log.d(TAG, "onActivityResult");
//        if (requestCode == REQUEST_EXIT) {
//            if (resultCode == RESULT_EXIT) {
//                this.finish();
//            }
//        }
//    }

    // called whenever we call invalidateOptionsMenu()
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        /*
        This function is called after invalidateOptionsMenu is called.
        This happens when the Navigation drawer is opened and closed.
         */
        boolean open = mDrawerLayout.isDrawerOpen(mDrawerList);
        hideMenuItems(menu, open);
        return super.onPrepareOptionsMenu(menu);
    }

    private void hideMenuItems(Menu menu, boolean open) {
        // hide the menu item(s) when the drawer is open
        // Refresh button on Status page
        MenuItem mi = menu.findItem(R.id.action_refresh);
        if ( mi != null )
            mi.setVisible(!open);

        // Add button on Notification page
        mi = menu.findItem(R.id.action_add_notification);
        if ( mi != null )
            mi.setVisible(!open);
        // Delete button on Notification page
        mi = menu.findItem(R.id.action_delete_notification);
        if ( mi != null )
            mi.setVisible(!open);

        // Delete button on Error page
        mi = menu.findItem(R.id.menu_delete);
        if ( mi != null )
            mi.setVisible(!open);

        // hide buttons on History / Chart page
        mi = menu.findItem(R.id.action_configure_chart);
        if (mi != null)
            mi.setVisible(!open);
        mi = menu.findItem(R.id.action_refresh_chart);
        if (mi != null)
            mi.setVisible(!open);
    }

    private class MyDrawerToggle extends ActionBarDrawerToggle {

        public MyDrawerToggle(Activity activity, DrawerLayout drawerLayout,
                              Toolbar toolbar,
                              int openDrawerContentDescRes,
                              int closeDrawerContentDescRes) {
            super(activity, drawerLayout, toolbar,
                    openDrawerContentDescRes, closeDrawerContentDescRes);
        }

        @Override
        public void onDrawerClosed(View drawerView) {
            super.onDrawerClosed(drawerView);
//            Log.d(TAG, "DrawerClosed");
            invalidateOptionsMenu();
            if (opened != null && !opened) {
                // drawer closed for the first time ever,
                // set that it has been closed
                opened = true;
                raApp.raprefs.set(OPENED_KEY, true);
            }
        }

        @Override
        public void onDrawerOpened(View drawerView) {
            super.onDrawerOpened(drawerView);
//            getSupportActionBar().setTitle(R.string.app_name);
            invalidateOptionsMenu();
        }

    }

    private class DrawerItemClickListener implements
            ListView.OnItemClickListener {

        @Override
        public void onItemClick(
                AdapterView<?> parent,
                View view,
                int position,
                long id) {
            // Perform action when a drawer item is selected
//            Log.d(TAG, "onDrawerItemClick: " + position);
            // when we have a list header, it counts as a position in the list
            // the first position to be exact. so we have to decrease the
            // position by 1 to get the proper item chosen in our list
            selectItem(position - 1);
        }

    }
} public class Utils {

    public final static int THEME_DARK = 0;
    public final static int THEME_LIGHT = 1;
    private static int iTheme = THEME_LIGHT;

    public static int getCurrentTheme() { return iTheme; }

    /**
     * Set the theme of the Activity, and restart it by creating a new Activity of the same type.
     */
    public static void changeToTheme(Activity activity, int theme)
    {
        iTheme = theme;
        activity.finish();
        activity.startActivity(new Intent(activity, activity.getClass()));
    }
    /** Set the theme of the activity, according to the configuration. */
    public static void onActivityCreateSetTheme(Activity activity)
    {
        switch (iTheme)
        {
            default:
            case THEME_LIGHT:
                activity.setTheme(R.style.AppTheme);
                break;
            case THEME_DARK:
//                activity.setTheme(R.style.AppThemeDark);
                break;
        }
    }
    public static void onActivityCreateSetTheme(Activity activity, int theme) {
        iTheme = theme;
        onActivityCreateSetTheme(activity);
    }

    public static SimpleDateFormat getDefaultDateFormat() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    }

    public static String getNotificationDateFormatString(long when) {
        DateFormat fmt = Utils.getNotificationDateFormat();
        return fmt.format(new Date(when));
    }

    private static DateFormat getNotificationDateFormat() {
        return DateFormat.getDateTimeInstance(DateFormat.SHORT,
                DateFormat.SHORT,
                Locale.getDefault());
    }

    public static DateFormat getOldDefaultDateFormat() {
        return DateFormat.getDateTimeInstance( DateFormat.DEFAULT,
                DateFormat.DEFAULT,
                Locale.getDefault() );
    }

    public static String getDisplayDate(String dbDateFormat) {
        // Assign the return value to be the date given
        // If there is a problem converting the date, just return the given date
        String displayDate = dbDateFormat;
        SimpleDateFormat dftProper = Utils.getDefaultDateFormat();
        Date d;
        try {
            // Parse the universal standard date format that is stored in the DB
            d = dftProper.parse(dbDateFormat);
            Calendar cal = Calendar.getInstance();
            cal.setTime(d);
            DateFormat dftDisplay = Utils.getOldDefaultDateFormat();
            // Convert to the display date format
            displayDate = dftDisplay.format(cal.getTime());
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return displayDate;
    }
}
import info.curtbinder.reefangel.controller.Controller;
import info.curtbinder.reefangel.db.ErrorTable;
import info.curtbinder.reefangel.db.NotificationTable;
import info.curtbinder.reefangel.db.StatusProvider;
import info.curtbinder.reefangel.db.StatusTable;
import info.curtbinder.reefangel.phone.AlarmReceiver;
import info.curtbinder.reefangel.phone.Globals;
import info.curtbinder.reefangel.phone.Permissions;
import info.curtbinder.reefangel.phone.R;
import info.curtbinder.reefangel.phone.RAApplication;
import info.curtbinder.reefangel.phone.MainActivity;
import info.curtbinder.reefangel.phone.Utils;
import java.util.Locale;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.JobIntentService;
import android.support.v4.app.NotificationCompat;
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 Curt Binder
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package info.curtbinder.reefangel.service;




public class NotificationService extends JobIntentService {

	private static final String TAG = NotificationService.class.getSimpleName();
	static final int JOB_ID = 1001;

	public static final String NOTIFICATION_CHANNEL_ID = "ra_notify_channel_01";
	public static final String ERROR_CHANNEL_ID = "ra_error_channel_02";

	private static RAApplication rapp;
	private String errorMessage;
	private String paramPrecision;
	private String[] parameters;

	public static void enqueueWork(Context context, Intent work) {
		enqueueWork(context, NotificationService.class, JOB_ID, work);
	}

	@Override
	protected void onHandleWork(@NonNull Intent intent) {
		rapp = (RAApplication) getApplication();
		String action = intent.getAction();
		if ( action.equals( MessageCommands.NOTIFICATION_CLEAR_INTENT ) ) {
			clearNotifications();

		} else if ( action.equals( MessageCommands.NOTIFICATION_LAUNCH_INTENT ) ) {
			clearNotifications();
			Intent i = getStatusActivity();
			getApplication().startActivity( i );
		} else if ( action.equals( MessageCommands.NOTIFICATION_INTENT ) ) {
			processNotifications();
		} else if ( action.equals( MessageCommands.NOTIFICATION_ERROR_INTENT ) ) {
			processError();
		}
	}

	private void processError ( ) {
		boolean fDisplayUpdate = true;
		if ( rapp.raprefs.isNotificationEnabled() ) {
			// Only proceed if notifications are enabled

			// check if can retry on errors
			if ( rapp.raprefs.isErrorRetryEnabled() ) {
				// increase error count as soon as we know it's an error
				rapp.increaseErrorCount();

				// we are to retry connection before displaying an error
				if ( rapp.canErrorRetry() ) {
					// if error count is less than the max,
					// we need to retry the communication
					String s =
							rapp.getString( R.string.messageErrorRetry,
											rapp.errorCount );
					broadcastUpdateStatus( s );
					fDisplayUpdate = false;
					try {
						Thread.sleep( rapp.raprefs
								.getNotificationErrorRetryInterval() );
					} catch ( InterruptedException e ) {
					}
					Intent i = new Intent( rapp, UpdateService.class );
					i.setAction( MessageCommands.QUERY_STATUS_INTENT );
					UpdateService.enqueueWork(rapp, i);
				}
				// otherwise if we have exceeded the max count, then we
				// display the error
			}

		}
		if ( fDisplayUpdate ) {
			// log the error in the error table
			String errorMessage = rapp.getErrorMessage();
			insertErrorMessage( errorMessage );

			// notify the user
			notifyUser();

			// update the app text
			broadcastUpdateStatus( errorMessage );
		}
	}

	private void broadcastUpdateStatus ( String status ) {
		Intent i = new Intent( MessageCommands.UPDATE_STATUS_INTENT );
		i.putExtra( MessageCommands.UPDATE_STATUS_ID, -1 );
		i.putExtra( MessageCommands.UPDATE_STATUS_STRING, status );
		rapp.sendBroadcast( i, Permissions.QUERY_STATUS );