/*
 * Copyright (c) 2015,2016 Oracle and/or its affiliates. All rights reserved.
 *
 * This software is dual-licensed to you under the MIT License (MIT) and
 * the Universal Permissive License (UPL). See the LICENSE file in the root
 * directory for license terms. You may choose either license, or both.
 */

package com.oracle.iot.sample;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;
import java.io.FileOutputStream;
import java.io.IOException;

import java.security.GeneralSecurityException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Locale;

import oracle.iot.client.DeviceModel;
import oracle.iot.client.device.Alert;
import oracle.iot.client.device.GatewayDevice;
import oracle.iot.client.device.VirtualDevice;

/**
 * This sample is a gateway that presents multiple simple sensors as virtual
 * devices to the IoT server.
 * <p>
 * The simple sensors are polled every half second and corresponding virtual
 * device is set.
 */
public class GatewayDeviceSampleService extends Service {
    private static final String ERROR_TAG = "IOT_ERROR";
    private static final String MSG_TAG = "IOT";
    private static GatewayDevice gatewayDevice;
    private Context context;

    private int maxMaxThermoThreshold = 70;
    private int minMaxThermoThreshold = 55;
    private int maxMaxHygroThreshold = 95;
    private int minMaxHygroThreshold = 80;

    private int rememberHygroMaxThreshold = 0;
    private int rememberThermoMaxThreshold = 0;

    private boolean messageSent = false;
    private String messageToBeSent = "";
    private boolean stopped = false;

    private Thread workerThread;
    private Thread sensorThread;

    private final static String HUMIDITY_SENSOR_MODEL_URN = "urn:com:oracle:iot:device:humidity_sensor";
    private final static String TEMPERATURE_SENSOR_MODEL_URN = "urn:com:oracle:iot:device:temperature_sensor";
    private static TemperatureSensor temperatureSensor;

    private static HumiditySensor humiditySensor;

    boolean useDevicePolicy = false;


    /**
     * This is a list of callbacks that have been registered with the
     * service.  Note that this is package scoped (instead of private) so
     * that it can be accessed more efficiently from inner classes.
     */
    final RemoteCallbackList<IGatewayDeviceSampleServiceCallback> mCallbacks
            = new RemoteCallbackList<IGatewayDeviceSampleServiceCallback>();

    NotificationManager mNM;
    PowerManager.WakeLock wakeLock;


    @Override
    public void onCreate() {

        mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        // Display a notification about us starting.
        showNotification(true);
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Select the interface to return.
        if (IGatewayDeviceSampleService.class.getName().equals(intent.getAction())) {
            return mBinder;
        }
        if (IGatewayDeviceSampleServiceSecondary.class.getName().equals(intent.getAction())) {
            return mSecondaryBinder;
        }
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                "IoTWakeLock");
        // power management: Keep the CPU on so we can continue to send messages back to the server
        wakeLock.acquire();
        stopped = false;
        context = getApplicationContext();
        Toast.makeText(this, "Service started.", Toast.LENGTH_LONG).show();
        workerThread = new Thread() {
            @Override
            public void run() {
                runSample();
            }
        };
        workerThread.start();

        return START_NOT_STICKY;
    }

    /**
     * The IGatewayDeviceSampleService Interface is defined through IDL
     */
    private final IGatewayDeviceSampleService.Stub mBinder =
            new IGatewayDeviceSampleService.Stub() {
        public void registerCallback(IGatewayDeviceSampleServiceCallback cb) {
            if (cb != null) mCallbacks.register(cb);
        }
        public void unregisterCallback(IGatewayDeviceSampleServiceCallback cb) {
            if (cb != null) mCallbacks.unregister(cb);
        }
    };
    /**
     * A secondary GD Sample Service interface to the service.
     */
    private final IGatewayDeviceSampleServiceSecondary.Stub mSecondaryBinder =
            new IGatewayDeviceSampleServiceSecondary.Stub() {
                public int getPid() {
                    return Process.myPid();
                }
                public String getMessage() {
                    messageSent = true;
                    return messageToBeSent;
                }
                public int getHygroMaxThreshold() {
                    return humiditySensor.getMaxThreshold();
                }
                public void increaseHygroMaxThreshold() {
                    int mt = humiditySensor.getMaxThreshold() + 1;
                    humiditySensor.setMaxThreshold(mt);
                    if(mt >= maxMaxHygroThreshold) {
                        display(getResources().getText(R.string.max_hygro_reached).toString());
                    }
                }
                public void decreaseHygroMaxThreshold() {
                    int mt = humiditySensor.getMaxThreshold() - 1;
                    humiditySensor.setMaxThreshold(mt);
                    if(mt <= minMaxHygroThreshold) {
                        display(getResources().getText(R.string.min_hygro_reached).toString());
                    }
                }
                public int getThermoMaxThreshold() {
                    return temperatureSensor.getMaxThreshold();
                }
                public void increaseThermoMaxThreshold() {
                    int mt = temperatureSensor.getMaxThreshold() + 1;
                    temperatureSensor.setMaxThreshold(mt);
                    if(mt >= maxMaxThermoThreshold) {
                        display(getResources().getText(R.string.max_thermo_reached).toString());
                    }
                }
                public void decreaseThermoMaxThreshold() {
                    int mt = temperatureSensor.getMaxThreshold() - 1;
                    temperatureSensor.setMaxThreshold(mt);
                    if(mt <= minMaxThermoThreshold) {
                        display(getResources().getText(R.string.min_thermo_reached).toString());
                    }
                }
                public void kill() {
                    // not used
                }
            };

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        Toast.makeText(this, "Task removed: " + rootIntent, Toast.LENGTH_LONG).show();
    }

    private static final int REPORT_MSG = 1;
    private static final int GD_MAX_HYGRO_MSG = 4;
    private static final int GD_MAX_THERMO_MSG = 5;
    private static final int ERROR_MSG = 6;

    /**
     * Our Handler used to execute operations on the main thread.  This is used
     * to schedule increments of our value.
     */
    private final Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {

                case REPORT_MSG: {
                    // Broadcast to all clients the new value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i=0; i<N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).valueChanged(messageToBeSent);
                        } catch (RemoteException e) {
                            // The RemoteCallbackList will take care of removing
                            // the dead object for us.
                        }
                    }
                    mCallbacks.finishBroadcast();

                } break;
                case GD_MAX_HYGRO_MSG: {
                    // Broadcast to all clients the max threshold value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i=0; i<N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).maxGdHygro(humiditySensor.getMaxThreshold() + "");
                        } catch (RemoteException e) {
                            // The RemoteCallbackList will take care of removing
                            // the dead object for us.
                        }
                    }
                    mCallbacks.finishBroadcast();

                } break;
                case GD_MAX_THERMO_MSG: {
                    // Broadcast to all clients the max threshold value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i=0; i<N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).maxGdThermo(temperatureSensor.getMaxThreshold() + ":"+temperatureSensor.getMinThreshold());
                        } catch (RemoteException e) {
                            // The RemoteCallbackList will take care of removing
                            // the dead object for us.
                        }
                    }
                    mCallbacks.finishBroadcast();

                } break;
                case ERROR_MSG: {
                    // Broadcast to all clients the new value.
                    final int N = mCallbacks.beginBroadcast();
                    for (int i = 0; i < N; i++) {
                        try {
                            mCallbacks.getBroadcastItem(i).errorMessage(messageToBeSent);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                    mCallbacks.finishBroadcast();
                } break;
                default:
                    super.handleMessage(msg);
            }
        }
    };

    public void runSample() {

        try {
            try {
                Log.d("DCD_DEBUG", "Before anything");
                SharedPreferences mSharedPref = getApplicationContext().getSharedPreferences(
                        getString(R.string.preference_file_key), Context.MODE_PRIVATE);
                useDevicePolicy = mSharedPref.getBoolean("useDevicePolicy", false);

                // Create the directly-connected device instance
                if (mSharedPref.getBoolean(getString(R.string.use_provided_bks), false)) {
                    Log.d("DCD_DEBUG", "Using existing BKS");
                    gatewayDevice = new GatewayDevice(context);
                } else {
                    SharedPreferences pref = getApplicationContext().getSharedPreferences(
                        getString(R.string.preference_file_key), Context.MODE_PRIVATE);
                    try (FileOutputStream fos = openFileOutput(getString(R.string.last_known), Context.MODE_PRIVATE)) {
                        fos.write(pref.getString(getString(R.string.ta_file_path), "").getBytes());
                    } catch (IOException ex) {
                        Log.d(ERROR_TAG, "Can not safe to internal storage");
                    }
                    Log.d("DCD_DEBUG", "Using BKS" + mSharedPref.getString(getString(R.string.ta_file_path),"") + "---" + mSharedPref.getString(getString(R.string.ta_password),"") + " --------");

                    gatewayDevice = new
                            GatewayDevice(
                                    mSharedPref.getString(getString(R.string.ta_file_path),""),
                                    mSharedPref.getString(getString(R.string.ta_password),""),
                                    context);
                }
            } catch (GeneralSecurityException dse) {
                // Tell the user
                displayError(dse.getMessage());
            }

            if (!gatewayDevice.isActivated()) {
                // If the device has not been activated, connect to the server
                // using client-credentials and activate the client to
                // obtain the private key. The private key is then persisted.
                gatewayDevice.activate();
            }

            temperatureSensor = new TemperatureSensor(gatewayDevice.getEndpointId() + "_Sample_TS");
            humiditySensor = new HumiditySensor(gatewayDevice.getEndpointId() + "_Sample_HS");

            // Get the device model instance.
            final DeviceModel temperatureDeviceModel =
                    gatewayDevice.getDeviceModel(TEMPERATURE_SENSOR_MODEL_URN);

            final DeviceModel humidityDeviceModel =
                    gatewayDevice.getDeviceModel(HUMIDITY_SENSOR_MODEL_URN);


            final String temperatureSensorEndpointId;
            final String humiditySensorEndpointId;

            Map<String, String> metaData = new HashMap<String, String>();
            metaData.put(
                    GatewayDevice.MANUFACTURER,
                    temperatureSensor.getManufacturer());
            metaData.put(
                    GatewayDevice.MODEL_NUMBER,
                    temperatureSensor.getModelNumber());
            metaData.put(
                    GatewayDevice.SERIAL_NUMBER,
                    temperatureSensor.getSerialNumber());

            temperatureSensorEndpointId =
                    gatewayDevice.registerDevice(
                            true, /* Default value for proposal. TODO: remove it after UI update */
                            temperatureSensor.getHardwareId(),
                            metaData,
                            TEMPERATURE_SENSOR_MODEL_URN);

            metaData = new HashMap<String, String>();
            metaData.put(
                    GatewayDevice.MANUFACTURER,
                    humiditySensor.getManufacturer());
            metaData.put(
                    GatewayDevice.MODEL_NUMBER,
                    humiditySensor.getModelNumber());
            metaData.put(
                    GatewayDevice.SERIAL_NUMBER,
                    humiditySensor.getSerialNumber());

            humiditySensorEndpointId =
                    gatewayDevice.registerDevice(
                            true, /* Default value for proposal. TODO: remove it after UI update */
                            humiditySensor.getHardwareId(),
                            metaData,
                            HUMIDITY_SENSOR_MODEL_URN);

            //
            // Create VirtualDevice instances to model the sensors
            // in the IoT Cloud Service
            //
            final VirtualDevice virtualizedTemperatureSensor =
                    gatewayDevice.createVirtualDevice(
                            temperatureSensorEndpointId,
                            temperatureDeviceModel);

            final VirtualDevice virtualizedHumiditySensor =
                    gatewayDevice.createVirtualDevice(
                            humiditySensorEndpointId,
                            humidityDeviceModel);

            //
            // For The temperatureSensor model, the min and max threshold values
            // can be written. Create a callback that will handle setting
            // the value on the temperature sensor device.
            //
            virtualizedTemperatureSensor.setOnChange(
                new VirtualDevice.ChangeCallback<VirtualDevice
                        >() {
                        @Override
                        public void onChange(VirtualDevice.ChangeEvent<VirtualDevice> event) {
                            VirtualDevice virtualDevice = event.getVirtualDevice();
                            VirtualDevice.NamedValue<?> namedValues = event.getNamedValue();
                            StringBuilder msg =
                                    new StringBuilder(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString());
                            msg.append(" : ");
                            msg.append(virtualDevice.getEndpointId());
                            msg.append(" : onChange : ");

                            boolean first = true;
                            for(VirtualDevice.NamedValue<?> namedValue = namedValues;
                                namedValue != null;
                                namedValue = namedValue.next()) {
                                final String attribute = namedValue.getName();
                                final Object value = namedValue.getValue();
                                if (!first) {
                                    msg.append(',');
                                } else {
                                    first = false;
                                }

                                if ("minThreshold".equals(attribute)) {
                                    int min = ((Integer)value).intValue();
                                    msg.append("\"minThreshold\"=");
                                    msg.append(min);
                                    temperatureSensor.setMinThreshold(min);
                                } else if ("maxThreshold".equals(attribute)) {
                                    int max = ((Integer)value).intValue();
                                    msg.append("\"maxThreshold\"=");
                                    msg.append(max);
                                    temperatureSensor.setMaxThreshold(max);
                                }
                            }

                            display(msg.toString());
                        }
                    }
            );

            //
            // The temperatureSensor model has a 'reset' action, which
            // resets the temperature sensor to factory defaults. Create
            // a callback that will handle calling reset on the temperature
            // sensor device.
            //
            virtualizedTemperatureSensor.setCallable("reset",
                    new VirtualDevice.Callable<Void>() {
                        @Override
                        public void call(VirtualDevice virtualDevice,
                                         Void not_used) {
                            display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                                    virtualDevice.getEndpointId() +
                                " : Call : reset");

                            temperatureSensor.reset();
                            // After the sensor is reset, next poll of the
                            // sensor will yield new min and max temp values
                            // and update the values in the VirtualDevice
                        }
                    });

            //
            // The temperatureSensor model has a 'power' action, which
            // takes a boolean argument: true to power on the simulated device,
            // false to power off the simulated device. Create a callback that
            // will handle calling power on and power off on the temperature
            // sensor device.
            //
            virtualizedTemperatureSensor.setCallable("power",
                    new VirtualDevice.Callable<Boolean>() {
                        @Override
                        public void call(VirtualDevice virtualDevice,
                                         Boolean on) {
                            display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : " +
                                    virtualDevice.getEndpointId() +
                                    " : Call : \"power\"=" + on);
                            boolean isOn = temperatureSensor.isPoweredOn();
                            if (on != isOn) {
                                temperatureSensor.power(on);
                            }

                            // Avoid re-entering the library, let the polling
                            // loop handle the attribute update.
                        }
                    });

            //
            // For the humiditySensor model, the maxThreshold attribute can be
            // written. Create a callback for setting the maxThreshold on the
            // humidity sensor device.
            //
            virtualizedHumiditySensor.setOnChange("maxThreshold",
                    new VirtualDevice.ChangeCallback<VirtualDevice>() {
                        @Override
                        public void onChange(VirtualDevice.ChangeEvent<VirtualDevice> event) {
                            VirtualDevice virtualDevice = event.getVirtualDevice();
                            VirtualDevice.NamedValue<?> namedValue = event.getNamedValue();
                            Integer value = (Integer)namedValue.getValue();
                            display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString()+ " : " +
                                virtualDevice.getEndpointId() +
                                " : onChange : \"maxThreshold\"=" + value);

                            humiditySensor.setMaxThreshold(value);
                        }
                    });


            //
            // Create a handler for errors that may be generated when trying
            // to set values on the virtual device. The same callback is used
            // for both virtual devices.
            //
            final VirtualDevice.ErrorCallback<VirtualDevice>
                errorCallback = new VirtualDevice.ErrorCallback<VirtualDevice
                    >() {
                @Override
                    public void onError(VirtualDevice.ErrorEvent event) {
                        VirtualDevice device =
                            (VirtualDevice)event.getVirtualDevice();
                        display(new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString() + " : onError : " +
                            device.getEndpointId() +
                            " : \"" + event.getMessage());
                }
            };

            virtualizedTemperatureSensor.setOnError(errorCallback);
            virtualizedHumiditySensor.setOnError(errorCallback);

            // Initialize the sensor to the device model's maxThreshold default value
            int defaultThreshold = virtualizedHumiditySensor.get("maxThreshold");
            humiditySensor.setMaxThreshold(defaultThreshold);
            rememberHygroMaxThreshold = humiditySensor.getMaxThreshold();

            // Initialize the sensor to the device model's min and
            // maxThreshold default value
            defaultThreshold = virtualizedTemperatureSensor.get("maxThreshold");
            temperatureSensor.setMaxThreshold(defaultThreshold);
            rememberThermoMaxThreshold = temperatureSensor.getMaxThreshold();
            defaultThreshold = virtualizedTemperatureSensor.get("minThreshold");
            temperatureSensor.setMinThreshold(defaultThreshold);

            sensorThread = new Thread(
                    new Runnable() {
                        public void run() {

                            // Only generate an alert when the threshold is crossed
                            boolean humidityAlerted = false;
                            boolean tempAlerted = false;
                            boolean wasOff = true;
                            long waitTime=0;
                            while(!stopped) {
                                try {
                                    //
                                    // Which sensor data is processed depends on whether or not policies are used.
                                    // Compare the code in the processSensorData method of the WithPolicies class
                                    // to that of the WithoutPolicies class.
                                    //
                                    final MainLogic mainLogic = useDevicePolicy
                                            ? new WithPolicies(humiditySensor, virtualizedHumiditySensor,
                                            temperatureSensor, virtualizedTemperatureSensor)
                                            :  new WithoutPolicies(humiditySensor, virtualizedHumiditySensor,
                                            temperatureSensor, virtualizedTemperatureSensor);

                                    mainLogic.processSensorData();

                                    Thread.sleep(waitTime);
                                    waitTime = 5000;

                                } catch (Exception e) {
                                    e.printStackTrace();
                                    if(e.getMessage() != null) {
                                        // Tell the user
                                        displayError(e.getMessage());
                                    } else {
                                        // Tell the user
                                        displayError("Error. No message.");
                                    }
                                }
                            }
                        }
                    });

            sensorThread.setDaemon(true);
            sensorThread.start();

        } catch (Exception e) {
            e.printStackTrace();
            Log.d("GD_ERROR", "runSample: -------------" + e.toString());
            if(e.getMessage() != null) {
                // Tell the user
                displayError(e.getMessage());
            } else {
                // Tell the user
                displayError("Error. No message.");
            }
        }
    }

    private abstract class MainLogic {

        protected final HumiditySensor humiditySensor;
        protected final VirtualDevice virtualHumiditySensor;
        protected final TemperatureSensor temperatureSensor;
        protected final VirtualDevice virtualTemperatureSensor;

        protected MainLogic(HumiditySensor humiditySensor,
                            VirtualDevice virtualHumiditySensor,
                            TemperatureSensor temperatureSensor,
                            VirtualDevice virtualTemperatureSensor) {
            this.humiditySensor = humiditySensor;
            this.virtualHumiditySensor = virtualHumiditySensor;
            this.temperatureSensor = temperatureSensor;
            this.virtualTemperatureSensor = virtualTemperatureSensor;
        }

        // This method allows for differentiation between the processing
        // of sensor data when policies are in use from when policies are
        // not in use.
        public abstract void processSensorData();
    }


    private class WithoutPolicies extends MainLogic {

        //
        //Create an Alert for max/min temperature threshold and another
        //for maximum humidity threshold. Alerts do not have to be
        //created new each time an alerting event occurs.
        //
        private final Alert tooHotAlert;
        private final Alert tooColdAlert;
        private final Alert tooHumidAlert;


        // Flags to ensure an alert is only generated when the threshold is crossed.
        private boolean humidityAlerted = false;
        private boolean tempAlerted = false;

        // Flag to know whether the temperature sensor was off and is now on.
        private boolean wasOff = true;

        // The previous minimum temperature.
        // If the new minimum temperature is less than the previous,
        // update "minTemp" in the virtual TemperatureSensor
        private double prevMinTemp = Double.MAX_VALUE;

        // The previous maximum temperature.
        // If the new maximum temperature is greater than the previous,
        // update "maxTemp" in the virtual TemperatureSensor
        private double prevMaxTemp = Double.MIN_VALUE;

        private WithoutPolicies(HumiditySensor humiditySensor,
                                VirtualDevice virtualHumiditySensor,
                                TemperatureSensor temperatureSensor,
                                VirtualDevice virtualTemperatureSensor) {
            super(humiditySensor, virtualHumiditySensor, temperatureSensor, virtualTemperatureSensor);

            tooHotAlert = virtualTemperatureSensor
                    .createAlert("urn:com:oracle:iot:device:temperature_sensor:too_hot");

            tooColdAlert = virtualTemperatureSensor
                    .createAlert("urn:com:oracle:iot:device:temperature_sensor:too_cold");

            tooHumidAlert = virtualHumiditySensor
                    .createAlert("urn:com:oracle:iot:device:humidity_sensor:too_humid");

        }

        //
        // When running the sample without policies, the sample must do more work.
        // The sample has to update minTemp and maxTemp, and generate alerts if
        // the sensor values exceed, or fall below, thresholds. This logic can
        // be handled by the client library with the right set of policies in
        // place. Compare this method to that of the WithPolicies inner class.
        //
        @Override
        public void processSensorData() {

            int humidity = humiditySensor.getHumidity();

            StringBuilder consoleMessage = new
                    StringBuilder(getFormattedTime())
                    .append(" : ")
                    .append(virtualHumiditySensor.getEndpointId())
                    .append(" : Set   : ")
                    .append("\"humidity\"=")
                    .append(humidity);

            // Set the virtualilzed humidity sensor value in an 'update'. If
            // the humidity maxThreshold has changed, that maxThreshold attribute
            // will be added to the update (see the block below). A call to
            // 'finish' will commit the update.
            virtualHumiditySensor.update()
                    .set("humidity", humidity);

            display(consoleMessage.toString());

            // Commit the update.
            virtualHumiditySensor.finish();

            int humidityThreshold = humiditySensor.getMaxThreshold();
            if (humidity > humidityThreshold) {
                if (!humidityAlerted) {
                    humidityAlerted = true;
                    consoleMessage = new StringBuilder(getFormattedTime())
                            .append(" : ")
                            .append(virtualHumiditySensor.getEndpointId())
                            .append(" : Alert : \"")
                            .append("humidity")
                            .append("\"=")
                            .append(humidity);
                    display(consoleMessage.toString());
                    tooHumidAlert
                            .set("humidity", humidity)
                            .raise();
                }
            } else {
                humidityAlerted = false;
            }

            if (temperatureSensor.isPoweredOn()) {

                //
                // Update the virtual device to reflect the new values.
                //
                final double temperature =
                        temperatureSensor.getTemp();
                final String unit =
                        temperatureSensor.getUnit();
                final double minTemp =
                        temperatureSensor.getMinTemp();
                final double maxTemp =
                        temperatureSensor.getMaxTemp();

                consoleMessage = new StringBuilder(getFormattedTime())
                        .append(" : ")
                        .append(virtualTemperatureSensor.getEndpointId())
                        .append(" : Set   : ");

                virtualTemperatureSensor.update();

                // Data model has temp range as [-20,80]. If data is out
                // of range, do not set the attribute in the virtual device.
                // Note: without this check, the set would throw an
                // IllegalArgumentException.
                if (-20 < temperature && temperature < 80) {
                    consoleMessage
                            .append("\"temp\"=")
                            .append(temperature);
                    virtualTemperatureSensor.set("temp", temperature);
                }

                // There is no need to set startTime or units if the value has not changed.
                if (wasOff) {
                    tempAlerted = false;
                    wasOff = false;

                    Date restartTime =
                            temperatureSensor.getStartTime();
                    //consoleMessage
                    //        .append(",\"unit\"=").append(unit)
                    //        .append(",\"startTime\"=").append(restartTime);

                    virtualTemperatureSensor
                            .set("startTime", restartTime)
                            .set("unit", unit);
                }

                int introLength = consoleMessage.length();

                // There is no need to set minTemp if the value has not changed.
                if (minTemp != prevMinTemp) {
                    prevMinTemp = minTemp;
                    consoleMessage.append(",\"minTemp\"=").append(minTemp);
                    virtualTemperatureSensor.set("minTemp", minTemp);
                }

                // There is no need to set maxTemp if the value has not changed.
                if (maxTemp != prevMaxTemp) {
                    prevMaxTemp = maxTemp;
                    consoleMessage.append(",\"maxTemp\"=").append(maxTemp);
                    virtualTemperatureSensor.set("maxTemp", maxTemp);
                }

                display(consoleMessage.toString());

                // finish() commits the update.
                virtualTemperatureSensor.finish();

                final int minThreshold = temperatureSensor.getMinThreshold();
                final int maxThreshold = temperatureSensor.getMaxThreshold();
                if (temperature > maxThreshold) {
                    if (!tempAlerted) {
                        tempAlerted = true;

                        consoleMessage = new StringBuilder(getFormattedTime())
                                .append(" : ")
                                .append(virtualTemperatureSensor.getEndpointId())
                                .append(" : Alert : ")
                                .append("\"temp\"=")
                                .append(temperature)
                                .append(",\"maxThreshold\"=")
                                .append(maxThreshold);

                        display(consoleMessage.toString());

                        tooHotAlert
                                .set("temp", temperature)
                                .set("maxThreshold", maxThreshold)
                                .raise();
                    }
                } else if (minThreshold < temperature) {
                    if (!tempAlerted) {
                        tempAlerted = true;
                        consoleMessage = new StringBuilder(getFormattedTime())
                                .append(" : ")
                                .append(virtualTemperatureSensor.getEndpointId())
                                .append(" : Alert : ")
                                .append("\"temp\"=")
                                .append(temperature)
                                .append(",\"minThreshold\"=")
                                .append(minThreshold);

                        display(consoleMessage.toString());

                        tooColdAlert
                                .set("temp", temperature)
                                .set("minThreshold", minThreshold)
                                .raise();
                    }
                } else {
                    tempAlerted = false;
                }

            } else {
                wasOff = true;
            }
        }
    }

    String getFormattedTime() {
        String dateStr = new SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(new Date()).toString();
        return dateStr;
    }

    private class WithPolicies extends MainLogic {

        private boolean wasOff = true;

        private WithPolicies(HumiditySensor humiditySensor,
                             VirtualDevice virtualHumiditySensor,
                             TemperatureSensor temperatureSensor,
                             VirtualDevice virtualTemperatureSensor) {
            super(humiditySensor, virtualHumiditySensor, temperatureSensor, virtualTemperatureSensor);
        }

        //
        // When running with policies, we only need to "offer" the
        // temperature and humidity values. Policies can be set on the
        // server to compute max and min temp, generate alerts, and
        // filter bad data - all of which have to be handled by the
        // sample code when running without policies.
        // Compare this implementation of processSensorData to that
        // of the WithoutPolicies inner class.
        //
        @Override
        public void processSensorData() {

            int humidity = humiditySensor.getHumidity();

            StringBuilder consoleMessage = new
                    StringBuilder(getFormattedTime())
                    .append(" : ")
                    .append(virtualHumiditySensor.getEndpointId())
                    .append(" : Offer : \"humidity\"=")
                    .append(humidity);

            display(consoleMessage.toString());

            virtualHumiditySensor.offer("humidity", humidity);

            if (temperatureSensor.isPoweredOn()) {

                //
                // Update the virtual device to reflect the new values.
                //
                final double temperature =
                        temperatureSensor.getTemp();

                consoleMessage = new StringBuilder(getFormattedTime())
                        .append(" : ")
                        .append(virtualTemperatureSensor.getEndpointId())
                        .append(" : Offer : ")
                        .append("\"temp\"=")
                        .append(temperature);


                virtualTemperatureSensor
                        .update()
                        .offer("temp", temperature);

                if (wasOff) {
                    wasOff = false;
                    final Date startTime = new Date();

                    // If there is no policy for the startTime attribute,
                    // the call to "offer" works just as a call to "set".
                    virtualTemperatureSensor
                            .offer("startTime", startTime);
                }

                display(consoleMessage.toString());

                virtualTemperatureSensor
                        .finish();

            } else {
                wasOff = true;
            }

        }
    }


    private void display(String s) {
        Log.d(MSG_TAG, s);
        messageToBeSent = s;
        messageSent = false;
        mHandler.sendEmptyMessage(REPORT_MSG);
        if(humiditySensor!=null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            mHandler.sendEmptyMessage(GD_MAX_HYGRO_MSG);
        }
        if(temperatureSensor!=null) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            mHandler.sendEmptyMessage(GD_MAX_THERMO_MSG);
        }
    }

    private void displayError(String string) {
        Log.d(ERROR_TAG, "Error Message: "+string);
        messageToBeSent = string;
        mHandler.sendEmptyMessage(ERROR_MSG);
        try {
            Thread.sleep(8000);
        } catch(InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.exit(-1);
    }

    @Override
    public void onDestroy() {
        stopped = true;
        // power management: let the CPU go to sleep
        if (wakeLock != null) wakeLock.release();

        try {
            sensorThread.join();
        } catch (InterruptedException e) {
			Thread.currentThread().interrupt();
        }
        try {
            workerThread.join();
        } catch (InterruptedException e) {
			Thread.currentThread().interrupt();
        }
        humiditySensor.setMaxThreshold(rememberHygroMaxThreshold);
        temperatureSensor.setMaxThreshold(rememberThermoMaxThreshold);
        // Cancel the persistent notification.
        mNM.cancelAll();
        showNotification(false);
        // Tell the user we stopped.
        Toast.makeText(this, R.string.gd_service_stopped, Toast.LENGTH_SHORT).show();

        // Unregister all callbacks.
        mCallbacks.kill();

        mHandler.removeMessages(REPORT_MSG);
        mHandler.removeMessages(GD_MAX_HYGRO_MSG);
        mHandler.removeMessages(GD_MAX_THERMO_MSG);
    }

    /**
     * Show a notification while this service is running.
     */
    private void showNotification(boolean serviceStarted) {
        // In this sample, we'll use the same text for the ticker and the expanded notification
        CharSequence text = serviceStarted ? getText(R.string.gd_service_started) :
                getText(R.string.gd_service_stopped);
        // The PendingIntent to launch our activity if the user selects this notification
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                new Intent(this, MainActivity.class), 0);
        // Set the info for the views that show in the notification panel.
        Notification notification = new Notification.Builder(this)
                .setSmallIcon(R.drawable.mini)  // the status icon
                .setTicker(text)  // the status text
                .setWhen(System.currentTimeMillis())  // the time stamp
                .setContentTitle(getText(R.string.gd_service_label))  // the label of the entry
                .setContentText(text)  // the contents of the entry
                .setContentIntent(contentIntent)  // The intent to send when the entry is clicked
                .build();
        // Send the notification.
        // We use a string id because it is a unique number.  We use it later to cancel.
        mNM.notify(serviceStarted ? R.string.gd_service_started :
                R.string.gd_service_stopped, notification);
    }
}
