/*
 * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.mobilyzer.measurements;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;

import com.mobilyzer.Checkin;
import com.mobilyzer.Config;
import com.mobilyzer.DeviceInfo;
import com.mobilyzer.MeasurementDesc;
import com.mobilyzer.MeasurementResult;
import com.mobilyzer.MeasurementTask;
import com.mobilyzer.MeasurementResult.TaskProgress;
import com.mobilyzer.exceptions.MeasurementError;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.MeasurementJsonConvertor;
import com.mobilyzer.util.PhoneUtils;
import com.mobilyzer.util.Util;

import android.content.SharedPreferences;
import android.net.TrafficStats;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.StringBuilderPrinter;



/**
 * This class measures the round trip times of packets as you vary the packet timings between them,
 * with the purpose of inferring RRC state transitions.
 * 
 * See "Characterizing Radio Resource Allocation for 3G Networks" by Feng et. al, IMC 2010 for a
 * full explanation of the methodology and goals.
 * 
 * TODO (sanae): Pause the RRC tasks when a checkin is performed, rather than pausing the checkin
 * while the RRC task is performed.
 * 
 * @author [email protected] (Sanae Rosen)
 * 
 */
public class RRCTask extends MeasurementTask {
  // Type name for internal use
  public static final String TYPE = "rrc";
  // Human readable name for the task
  public static final String DESCRIPTOR = "rrc";
  public static String TAG = "MobiPerf_RRC_INFERENCE";

  private volatile boolean stopFlag;
  private long duration;
  
  //Track data consumption for this task to avoid exceeding user's limit
  public static long data_consumed = 0;


  /**
   * Stores parameters for the RRC inference task
   * 
   * @author [email protected] (Sanae Rosen)
   * 
   */
  public static class RRCDesc extends MeasurementDesc {
    private static String HOST = "www.google.com";

    // Default echo server name and port to measure the RTT to infer RRC state
    private static int PORT = 50000;
    private static String ECHO_HOST = "ep2.eecs.umich.edu";
    // Perform RTT measurements every GRANULARITY ms
    public int GRANULARITY = 500;
    // MIN / MAX is the echo packet size
    int MIN = 0;
    int MAX = 1024;
    // Default total number of measurements
    int size = 31;
    // Echo server / port, and target to perform the upper-layer tasks
    public String echoHost = ECHO_HOST;
    public String target = HOST;
    int port = PORT;

    long testId; // unique value for this set of tests

    // Default threshold to repeat each RTT measurement because of background traffic
    int GIVEUP_THRESHHOLD = 15;

    // server controled variable
    boolean DNS = true;
    boolean TCP = true;
    boolean HTTP = true;
    boolean RRC = true;
    boolean SIZES = true;

    // Whether RRC result is visible to users
    public boolean RESULT_VISIBILITY = false;

    /*
     * For the upper-layer tests, a series of tests are made for different inter-packet intervals,
     * in order. "Times" indicates the inter-packet intervals, the other fields store the results.
     * All must be the same size.
     */
    Integer[] times; // The times where the above tests were made, in units of GRANULARITY.
    int sizeGranularity = 200; // the spacing between sizes to test
    int[] httpTest; // The results of the HTTP test performed at each time
    int[] dnsTest; // likewise, for the DNS test
    int[] tcpTest; // likewise, for the TCP test

    // Whether or not to run the upper layer tests, i.e. the HTTP, TCP and DNS tests.
    // Disabling this flag will disable all upper layer tests.
    private boolean runUpperLayerTests = false;

    /*
     * Default times between packets for which the upper layer tests are performed. Later, these
     * times will be replaced by times from the model. These times are GRANULARITY milliseconds: if
     * GRANULARITY is 500, then a default time of 6 means measurements are taken 500 ms apart.
     */
    Integer[] defaultTimesULTasks = new Integer[] {0, 2, 4, 8, 12, 16, 22};


    public RRCDesc(String key, Date startTime, Date endTime, double intervalSec, long count,
        long priority, int contextIntervalSec, Map<String, String> params) {
      super(RRCTask.TYPE, key, startTime, endTime, intervalSec, count, priority,
          contextIntervalSec, params);
      initializeParams(params);
    }

    protected RRCDesc(Parcel in) {
      super(in);
//      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      echoHost = in.readString();
      target = in.readString();
      MIN = in.readInt();
      MAX = in.readInt();
      port = in.readInt();
      size = in.readInt();
      sizeGranularity = in.readInt();
      DNS = in.readByte() != 0;
      HTTP = in.readByte() != 0;
      TCP = in.readByte() != 0;
      RRC = in.readByte() != 0;
      SIZES = in.readByte() != 0;
      RESULT_VISIBILITY = in.readByte() != 0;
      GIVEUP_THRESHHOLD = in.readInt();
      Object[] temp = in.readArray(Integer.class.getClassLoader());
      times = Arrays.copyOf(temp, temp.length, Integer[].class);
    }

    public static final Parcelable.Creator<RRCDesc> CREATOR = new Parcelable.Creator<RRCDesc>() {

      @Override
      public RRCDesc createFromParcel(Parcel in) {
        return new RRCDesc(in);
      }

      @Override
      public RRCDesc[] newArray(int size) {
        return new RRCDesc[size];
      }

    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
      super.writeToParcel(dest, flags);
      dest.writeString(echoHost);
      dest.writeString(target);
      dest.writeInt(MIN);
      dest.writeInt(MAX);
      dest.writeInt(port);
      dest.writeInt(size);
      dest.writeInt(sizeGranularity);
      dest.writeByte((byte) (DNS ? 1 : 0));
      dest.writeByte((byte) (HTTP ? 1 : 0));
      dest.writeByte((byte) (TCP ? 1 : 0));
      dest.writeByte((byte) (RRC ? 1 : 0));
      dest.writeByte((byte) (SIZES ? 1 : 0));
      dest.writeByte((byte) (RESULT_VISIBILITY ? 1 : 0));
      dest.writeInt(GIVEUP_THRESHHOLD);
      dest.writeArray(times);
    }

    public MeasurementResult getResults(MeasurementResult result) {
      if (HTTP) result.addResult("http", httpTest);
      if (TCP) result.addResult("tcp", tcpTest);
      if (DNS) result.addResult("dns", dnsTest);
      result.addResult("times", times);
      return result;
    }

    public void displayResults(StringBuilderPrinter printer) {
      String DEL = "\t", toprint = DEL + DEL;
      for (int i = 1; i <= times.length; i++) {
        toprint += DEL + " | state" + i;
      }
      toprint += " |";
      int oneLineLen = toprint.length();
      toprint += "\n";
      // seperator
      for (int i = 0; i < oneLineLen; i++) {
        toprint += "-";
      }
      toprint += "\n";
      if (HTTP) {
        toprint += "HTTP (ms)" + DEL;
        for (int i = 0; i < httpTest.length; i++) {
          toprint += DEL + " | " + Integer.toString(httpTest[i]);
        }
        toprint += " |\n";
        for (int i = 0; i < oneLineLen; i++) {
          toprint += "-";
        }
        toprint += "\n";
      }

      if (DNS) {
        toprint += "DNS (ms)" + DEL;
        for (int i = 0; i < dnsTest.length; i++) {
          toprint += DEL + " | " + Integer.toString(dnsTest[i]);
        }
        toprint += " |\n";
        for (int i = 0; i < oneLineLen; i++) {
          toprint += "-";
        }
        toprint += "\n";
      }

      if (TCP) {
        toprint += "TCP (ms)" + DEL;
        for (int i = 0; i < tcpTest.length; i++) {
          toprint += DEL + " | " + Integer.toString(tcpTest[i]);
        }
        toprint += " |\n";
        for (int i = 0; i < oneLineLen; i++) {
          toprint += "-";
        }
        toprint += "\n";
      }

      toprint += "Timers (s)";
      for (int i = 0; i < times.length; i++) {
        double curTime = (double) times[i] * (double) GRANULARITY / 1000.0;
        toprint += DEL + " | " + String.format("%.2f", curTime);
      }
      toprint += " |\n";
      printer.println(toprint);
    }

    @Override
    public String getType() {
      return RRCTask.TYPE;
    }

    /**
     * Given the parameters fetched from the server, sets up the parameters as needed for the upper
     * layer tests, i.e. the application layer tests.
     */
    @Override
    protected void initializeParams(Map<String, String> params) {

      // In this case, we fall back to the default values defined above.
      if (params == null) {
        return;
      }

      // The parameters for the echo server
      this.echoHost = params.get("echo_host");
      this.target = params.get("target");
      if (this.echoHost == null) {
        this.echoHost = ECHO_HOST;
      }
      if (this.target == null) {
        this.target = HOST;
      }
      Logger.d("param: echo_host " + this.echoHost);
      Logger.d("param: target " + this.target);

      try {
        String val = null;
        // Size of the small packet
        if ((val = params.get("min")) != null && val.length() > 0 && Integer.parseInt(val) > 0) {
          this.MIN = Integer.parseInt(val);
        }
        Logger.d("param: Min " + this.MIN);
        // Size of the large packet
        if ((val = params.get("max")) != null && val.length() > 0 && Integer.parseInt(val) > 0) {
          this.MAX = Integer.parseInt(val);
        }
        Logger.d("param: MAX " + this.MAX);
        // Echo server port
        if ((val = params.get("port")) != null && val.length() > 0 && Integer.parseInt(val) > 0) {
          this.port = Integer.parseInt(val);
        }
        Logger.d("param: port " + this.port);
        // Number of tests to run from the RRC test
        if ((val = params.get("size")) != null && val.length() > 0 && Integer.parseInt(val) > 0) {
          this.size = Integer.parseInt(val);
        }
        Logger.d("param: size " + this.size);
        // When testing size dependence, increase the
        if ((val = params.get("size_granularity")) != null && val.length() > 0
            && Integer.parseInt(val) > 0) {
          this.sizeGranularity = Integer.parseInt(val);
        }
        Logger.d("param: size_granularity " + this.sizeGranularity);
        // Whether or not to run the DNS test
        if ((val = params.get("dns")) != null && val.length() > 0) {
          this.DNS = Boolean.parseBoolean(val);
        }
        Logger.d("param: DNS " + this.DNS);
        // Whether or not to run the HTTP test
        if ((val = params.get("http")) != null && val.length() > 0) {
          this.HTTP = Boolean.parseBoolean(val);
        }
        Logger.d("param: HTTP " + this.HTTP);
        // Whether or not to run the TCP test
        if ((val = params.get("tcp")) != null && val.length() > 0) {
          this.TCP = Boolean.parseBoolean(val);
        }
        Logger.d(params.get("rrc"));
        Logger.d("param: TCP " + this.TCP);
        // Whether or not to run the RRC inference task
        if ((val = params.get("rrc")) != null && val.length() > 0) {
          this.RRC = Boolean.parseBoolean(val);
        }
        Logger.d("param: RRC " + this.RRC);
        if ((val = params.get("measure_sizes")) != null && val.length() > 0) {
          this.SIZES = Boolean.parseBoolean(val);
        }
        Logger.d("param: SIZES " + this.SIZES);
        // Whether the RRC result is visible to users
        if ((val = params.get("result_visibility")) != null && val.length() > 0) {
          this.RESULT_VISIBILITY = Boolean.parseBoolean(val);
        }
        Logger.d("param: visibility " + this.RESULT_VISIBILITY);
        // How many times to retry a test when interrupted by background traffic
        if ((val = params.get("giveup_threshhold")) != null && val.length() > 0
            && Integer.parseInt(val) > 0) {
          this.GIVEUP_THRESHHOLD = Integer.parseInt(val);
        }
        Logger.d("param: GIVEUP_THRESHHOLD " + this.GIVEUP_THRESHHOLD);

        // Default assumed timers for the upper layer tests (HTTP, DNS, TCP),
        // in units of GRANULARITY. These are set via a comma-separated list
        // of numbers.
        if ((val = params.get("default_extra_test_timers")) != null && val.length() > 0) {
          String[] timesString = val.split("\\s*,\\s*");
          List<String> stringList = new ArrayList<String>(Arrays.asList(timesString));
          List<Integer> intList = new ArrayList<Integer>();
          Iterator<String> iterator = stringList.iterator();
          while (iterator.hasNext()) {
            intList.add(Integer.parseInt(iterator.next()));
          }
          times = (Integer[]) intList.toArray(new Integer[intList.size()]);

        }
        if (times == null) {
          times = defaultTimesULTasks;
        }
      } catch (NumberFormatException e) {
        throw new InvalidParameterException(" RRCTask cannot be created due to invalid params");
      }

      if (size == 0) {
        // 31 tests, by default. From 0s to 15s inclusive, in half-second intervals.
        size = 31;
      }
    }

    /**
     * For the arrays holding the results for the upper layer tests, we need to initialize them to
     * be the same size as the number of tests we run. -1 means uninitialized.
     * 
     * @param size Number of results to store
     */
    public void initializeExtraTaskResults(int size) {
      httpTest = new int[size];
      dnsTest = new int[size];
      tcpTest = new int[size];
      for (int i = 0; i < size; i++) {
        httpTest[i] = -1;
        dnsTest[i] = -1;
        tcpTest[i] = -1;
      }
      runUpperLayerTests = true;
    }

    /**
     * Given an interpacket interval and a round trip time from an HTTP test, store that value.
     * 
     * @param index Index into measurement result array, corresponding to an interpacket interval
     * @param rtt Time for HTTP test to complete, in milliseconds
     * @throws MeasurementError
     */
    public void setHttp(int index, int rtt) throws MeasurementError {
      if (!runUpperLayerTests) {
        throw new MeasurementError("Data class not initialized");
      }
      httpTest[index] = rtt;
    }

    /**
     * Given an interpacket interval and a round trip time from a TCP test, store that value.
     * 
     * @param index Index into measurement result array, corresponding to an interpacket interval
     * @param rtt Time for the TCP test to complete, in milliseconds
     * @throws MeasurementError
     */
    public void setTcp(int index, int rtt) throws MeasurementError {
      if (!runUpperLayerTests) {
        throw new MeasurementError("Data class not initialized");
      }
      tcpTest[index] = rtt;
    }

    /**
     * Given an interpacket interval and a round trip time from a DNS test, store that value.
     * 
     * @param index Index into measurement result array, corresponding to an interpacket interval
     * @param rtt Time for the DNS test to complete, in milliseconds
     * @throws MeasurementError
     */
    public void setDns(int index, int rtt) throws MeasurementError {
      if (!runUpperLayerTests) {
        throw new MeasurementError("Data class not initialized");
      }
      dnsTest[index] = rtt;
    }

  }

  /**
   * Stores data from the tests on the effect of packet size on RRC state related delays
   * 
   * @author [email protected] (Sanae Rosen)
   */
  public static class RrcSizeTestData {
    int time; // Interval between packets
    int size; // Size of packets sent
    long result; // Round trip time, in milliseconds
    long testId; // A unique id associated with a set of measurements

    /**
     * 
     * @param time The inter-packet interval
     * @param size The packet size, in bytes
     * @param result The round trip time for that packet size and inter-packet interval
     * @param testId The unique id for this set of tests
     */
    public RrcSizeTestData(int time, int size, long result, long testId) {
      this.time = time;
      this.size = size;
      this.result = result;
      this.testId = testId;
    }

    public JSONObject toJSON(String networktype, String phoneId) throws JSONException {
      JSONObject entry = new JSONObject();
      entry.put("network_type", networktype);
      entry.put("phone_id", phoneId);
      entry.put("test_id", testId);
      entry.put("time_delay", time);
      entry.put("size", size);
      entry.put("result", result);

      return entry;
    }

  }

  /**
   * Store the results of our RRC test.
   * 
   * Data is stored as a list of results indexed by the test number. Tests are performed in order
   * with increasing inter-packet intervals and results and data about the tests are stored here.
   * 
   * @author [email protected] (Sanae Rosen)
   * 
   */
  public static class RRCTestData {

    // Round-trip times, in ms
    int[] rttsSmall;
    int[] rttsLarge;
    // Packets lost for each test
    int[] packetsLostSmall;
    int[] packetsLostLarge;
    // Signal strengths at time of each test
    int[] signalStrengthSmall;
    int[] signalStrengthLarge;
    // Error Counts from each test
    int[] errorCountSmall;
    int[] errorCountLarge;

    ArrayList<RrcSizeTestData> packetSizes;

    // Unique incrementing value that identifies this set of tests.
    long testId;

    /**
     * @param size Number of tests to run (each corresponding to an interpacket interval, increasing
     *        in increments of 500 ms)
     * @param testId Unique ID for this set of tests
     */
    public RRCTestData(int size, long testId) {
      size = size + 1;


      rttsSmall = new int[size];
      rttsLarge = new int[size];
      packetsLostSmall = new int[size];
      packetsLostLarge = new int[size];
      signalStrengthSmall = new int[size];
      signalStrengthLarge = new int[size];
      errorCountLarge = new int[size];
      errorCountSmall = new int[size];

      this.testId = testId;

      packetSizes = new ArrayList<RrcSizeTestData>();

      // Set default values
      for (int i = 0; i < rttsSmall.length; i++) {
        // 7000 is the cutoff for timeouts.
        // This makes the model-building script treat no data and timeouts the same.
        rttsSmall[i] = 7000;
        rttsLarge[i] = 7000;
        packetsLostSmall[i] = -1;
        packetsLostLarge[i] = -1;
        signalStrengthSmall[i] = -1;
        signalStrengthLarge[i] = -1;
        errorCountSmall[i] = -1;
        errorCountLarge[i] = -1;
      }
    }

    public long testId() {
      return testId;
    }

    public String[] toJSON(String networktype, String phoneId) {
      String[] returnval = new String[rttsSmall.length];
      try {
        for (int i = 0; i < rttsSmall.length; i++) {
          JSONObject subtest = new JSONObject();
          subtest.put("rtt_low", rttsSmall[i]);
          subtest.put("rtt_high", rttsLarge[i]);
          subtest.put("lost_low", packetsLostSmall[i]);
          subtest.put("lost_high", packetsLostLarge[i]);
          subtest.put("signal_low", signalStrengthSmall[i]);
          subtest.put("signal_high", signalStrengthLarge[i]);
          subtest.put("error_low", errorCountSmall[i]);
          subtest.put("error_high", errorCountLarge[i]);
          subtest.put("network_type", networktype);
          subtest.put("time_delay", i);
          subtest.put("test_id", testId);
          subtest.put("phone_id", phoneId);
          returnval[i] = subtest.toString();
          Logger.w("Test ID for rrc inference test was " + this.testId);
        }
      } catch (JSONException e) {
        Logger.e("Error converting RRC data to JSON");
      }
      return returnval;
    }

    /**
     * Erase all data corresponding to a set of results with a particular interpacket interval
     * 
     * @param index Index into the array of results.
     */
    public void deleteItem(int index) {
      rttsSmall[index] = -1;
      rttsLarge[index] = -1;
      packetsLostSmall[index] = -1;
      packetsLostLarge[index] = -1;
      signalStrengthSmall[index] = -1;
      signalStrengthLarge[index] = -1;
      errorCountSmall[index] = -1;
      errorCountLarge[index] = -1;
    }

    /**
     * Save the data resulting from a given test.
     * 
     * @param index Index into the array of results.
     * @param rttMax Round trip time for large packets (generally 1KB)
     * @param rttMin Round time time for small packets (generally empty)
     * @param numPacketsLostMax Number of packets lost for the set of large packets, out of 10
     * @param numPacketsLostMin Likewise, for the small packets
     * @param errorHigh Error count for the set of large packets
     * @param errorLow Likewise, for the small packets
     * @param signalHigh The signal strength when sending the large packets
     * @param signalLow Likewise, for the small packets
     */
    public void updateAll(int index, int rttMax, int rttMin, int numPacketsLostMax,
        int numPacketsLostMin, int errorHigh, int errorLow, int signalHigh, int signalLow) {
      this.rttsLarge[index] = (int) rttMax;
      this.rttsSmall[index] = (int) rttMin;
      this.packetsLostLarge[index] = numPacketsLostMax;
      this.packetsLostSmall[index] = numPacketsLostMin;
      this.errorCountLarge[index] = errorHigh;
      this.errorCountSmall[index] = errorLow;
      this.signalStrengthLarge[index] = signalHigh;
      this.signalStrengthSmall[index] = signalLow;
    }

    public void setRrcSizeTestData(int index, int size, long result, long testId)
        throws MeasurementError {
      packetSizes.add(new RrcSizeTestData(index, size, result, testId));
    }

    public String[] sizeDataToJSON(String networkType, String phoneId) {
      String[] returnval = new String[packetSizes.size()];
      try {
        for (int i = 0; i < packetSizes.size(); i++) {
          returnval[i] = packetSizes.get(i).toJSON(networkType, phoneId).toString();
        }
      } catch (JSONException e) {
        Logger.e("Error converting RRC data to JSON");
      }
      return returnval;
    }
  }

  /**
   * Class for tracking if there has been interfering traffic.
   * 
   * We need to know how many packets are expected. We can then use the global packet counters to
   * see if more packets are sent than expected.
   * 
   * @author [email protected] (Sanae Rosen)
   * 
   */
  public static class PacketMonitor {
    private long[] packetsFirst;
    private long[] packetsLast;
    boolean bySize = false;

    /**
     * Initialize immediately before use. Values are time-sensitive.
     */
    PacketMonitor() {
      readCurrentPacketValues();
    }

    void setBySize() {
      bySize = true;
    }

    void readCurrentPacketValues() {
      packetsFirst = getPacketsSent();
    }

    /**
     * Call this to determine if packets have been sent since initializing.
     * 
     * @param expectedRcv Number of packets expected to be received by the device since
     *        initialization
     * @param expectedSent Number of packets expected to be sent by the device since initialization
     * @return Whether or not there is interfering traffic
     */
    boolean isTrafficInterfering(int expectedRcv, int expectedSent) {
      packetsLast = getPacketsSent();

      long rcvPackets = (packetsLast[0] - packetsFirst[0]);
      long sentPackets = (packetsLast[1] - packetsFirst[1]);
      if (rcvPackets <= expectedRcv && sentPackets <= expectedSent) {
        Logger.d("No competing traffic, continue");
        return false;
      }
      Logger.d("Competing traffic, retry");

      return true;
    }

    /**
     * Determine how many packets, so far, have been sent (the contents of /proc/net/dev/). This is
     * a global value. We use this to determine if any other app anywhere on the phone may have sent
     * interfering traffic that might have changed the RRC state without our knowledge.
     * 
     * @return Two values: number of bytes or packets received at index 0, followed by the number
     *         sent at index 1.
     */
    public long[] getPacketsSent() {
      long[] retval = {-1, -1};
      if (bySize) {
        retval[0] = TrafficStats.getMobileRxBytes();
        retval[1] = TrafficStats.getMobileTxBytes();

      } else {
        retval[0] = TrafficStats.getMobileRxPackets();
        retval[1] = TrafficStats.getMobileTxPackets();
      }

      return retval;
    }
  }


  @SuppressWarnings("rawtypes")
  public static Class getDescClass() throws InvalidClassException {
    return RRCDesc.class;
  }

  public RRCTask(MeasurementDesc desc) {
    super(new RRCDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count,
        desc.priority, desc.contextIntervalSec, desc.parameters));
    this.stopFlag = false;
    this.duration = Config.DEFAULT_RRC_TASK_DURATION;
  }

  protected RRCTask(Parcel in) {
    super(in);
    stopFlag = in.readByte() != 0;
    duration = in.readLong();
  }

  public static final Parcelable.Creator<RRCTask> CREATOR = new Parcelable.Creator<RRCTask>() {
    public RRCTask createFromParcel(Parcel in) {
      return new RRCTask(in);
    }

    public RRCTask[] newArray(int size) {
      return new RRCTask[size];
    }
  };

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    super.writeToParcel(dest, flags);
    dest.writeByte((byte) (stopFlag ? 1 : 0));
    dest.writeLong(duration);
  }



  @Override
  public MeasurementTask clone() {
    MeasurementDesc desc = this.measurementDesc;
    RRCDesc newDesc =
        new RRCDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count,
            desc.priority, desc.contextIntervalSec, desc.parameters);
    return new RRCTask(newDesc);
  }

  @Override
  public MeasurementResult[] call() throws MeasurementError {
    try {
      RRCDesc desc = runInferenceTests();
      MeasurementResult[] mrArray = new MeasurementResult[1];
      mrArray[0] = constructResultStandard(desc);;
      return mrArray;
    } catch(MeasurementError e) {
      if (e.getMessage().equals("Rescheduled")) {
        stopFlag=false;
        PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
        MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, 
            phoneUtils.getDeviceProperty(this.getKey()), RRCTask.TYPE, 
            System.currentTimeMillis() * 1000, TaskProgress.RESCHEDULED, this.measurementDesc);
        Logger.i(MeasurementJsonConvertor.toJsonString(result));
        MeasurementResult[] mrArray= new MeasurementResult[1];
        mrArray[0]=result;
        return mrArray;
      }
      else {
        throw e;
      }
    }
  }

  @Override
  public String getDescriptor() {
    return DESCRIPTOR;
  }

  @Override
  public String getType() {
    return RRCTask.TYPE;
  }

  @Override
  public boolean stop() {
    stopFlag = true;
    return true;
  }

  /**
   * Helper function to construct MeasurementResults to submit to the server
   * 
   * @param desc The data collected for the RRC test to be converted into something understandable
   *        by the server.
   * @return A data structure that can be sent to the server. Contains only the results of the TCP,
   *         DNS and HTTP tests: the others are sent via a separate mechanism as they are stored
   *         separately.
   */
  private MeasurementResult constructResultStandard(RRCDesc desc) {
    PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
    TaskProgress taskProgress = TaskProgress.COMPLETED;
    MeasurementResult result =
        new MeasurementResult(phoneUtils.getDeviceInfo().deviceId,
          phoneUtils.getDeviceProperty(this.getKey()),
          RRCTask.TYPE, System.currentTimeMillis() * 1000,
          taskProgress, this.measurementDesc);

    if (desc.runUpperLayerTests) {
      result = desc.getResults(result);
    }

    Logger.i(MeasurementJsonConvertor.toJsonString(result));
    return result;
  }

  /**
   * The core RRC inference functionality is in this function. The steps involved can be summarized
   * as follows:
   * <ol>
   * <li>Fetch the last model generated by the server, if it exists.</li>
   * <li>Check we are on a cellular network, otherwise abort.</li>
   * <li>Inform all other tasks that they should delay network traffic until later.</li>
   * <li>For every upper layer test, if upper layer tests and that test specifically are enabled,
   * run that test.</li>
   * </ol>
   * 
   * @return A data structure containing all the results and metadata surrounding the RRC inference
   *         tests performed.
   * @throws MeasurementError
   */
  private RRCDesc runInferenceTests() throws MeasurementError {
    data_consumed = 0;
    
    // Fetch the existing model from the server, if it exists
    RRCDesc desc = (RRCDesc) measurementDesc;
    desc.testId = getTestId(PhoneUtils.getGlobalContext());
    Logger.w("Test ID set to " + desc.testId);
    PhoneUtils utils = PhoneUtils.getPhoneUtils();
    desc.initializeExtraTaskResults(desc.times.length);

    // Check to make sure we are on a valid (i.e. cellular) network
    if (utils.getNetwork() == "UNKNOWN" || utils.getNetwork() == "WIRELESS") {
      Logger.d("Returning: network is" + utils.getNetwork() + " rssi " + utils.getCurrentRssi());
      return desc;
    }

    RRCTestData data = new RRCTestData(desc.size, desc.testId);
    try {
      /*
       * Suspend all other tasks performed by the app as they can interfere. Although we have a
       * built-in check where we abort if traffic in the background interferes, in the past people
       * have scheduled other tests to be every 5 minutes, which can cause the RRC task to never
       * successfully complete without having to abort.
       */
      // If the RRC task is enabled
      if (desc.RRC) {
        // Set up the connection to the echo server
        Logger.d("Active inference: about to begin");
        Logger.d(desc.echoHost + ":" + desc.port);
        InetAddress serverAddr = InetAddress.getByName(desc.echoHost);

        // Perform the RRC timer and latency inference task
        Logger.d("Demotion inference: about to begin");
        desc = inferDemotion(serverAddr, desc, data, utils);

        Logger.d("About to save data");

        try {
          Logger.w("RRC: update the model on the GAE datastore");
          uploadRrcInferenceData(data);
          Logger.d("Saving data complete");
        } catch (IOException e) {
          e.printStackTrace();
          Logger.e("Data not saved: " + e.getMessage());
        }
      }

      // Check if the upper layer tasks are enabled
      if (desc.runUpperLayerTests) {
        if (desc.DNS) {
          Logger.w("Start DNS task");
          // Test the dependence of DNS latency on the RRC state, using
          // the previously constructed model if available.
          runDnsTest(desc.times, desc);
        }
        if (desc.TCP) {
          Logger.w("Start TCP task");
          // Test the dependence of TCP latency on the RRC state.
          runTCPHandshakeTest(desc.times, desc);
        }
        if (desc.HTTP) {
          Logger.w("Start HTTP task");
          // Test the dependence of HTTP latency on the RRC state.
          runHTTPTest(desc.times, desc);
        }

        if (desc.SIZES) {
          Logger.w("Start size dependence task");
          runSizeThresholdTest(desc.times, desc, data, desc.testId);
          uploadRrcInferenceSizeData(data);
        }
      }


    } catch (UnknownHostException e) {
      e.printStackTrace();
    } catch (SocketException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (MeasurementError e) {
      if (e.getMessage().equals("Rescheduled")) {
        try {
          // If the RRC task is enabled and we get partial data
          if (desc.RRC && data.rttsSmall[0] != 7000) {
            Logger.i("RRC Reschedule: update the model on the GAE datastore");
            uploadRrcInferenceData(data);
            Logger.d("RRC Reschedule: Saving data complete");
          }
          else {
            Logger.i("RRC Reschedule: no model available");
          }
        } catch (IOException ee) {
          ee.printStackTrace();
          Logger.e("Data not saved: " + ee.getMessage());
        }
        // Check if the upper layer tasks are enabled and we get partial results
        if (desc.runUpperLayerTests && desc.SIZES && !data.packetSizes.isEmpty()) {
          Logger.i("RRC Reschedule: update the size on the GAE datastore, array size " + data.packetSizes.size());
          uploadRrcInferenceSizeData(data);
          Logger.d("RRC Reschedule: Saving data complete");
        }
        else {
          Logger.i("RRC Reschedule: no size information available");
        }
      }
      throw e;
    }

    return desc;
  }

  /**
   * Impact of packet sizes on rrc inference results.
   * 
   * @param sizeData Contains data to upload
   */
  private void uploadRrcInferenceSizeData(RRCTestData sizeData) {
    PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
    Checkin checkin = new Checkin(PhoneUtils.getGlobalContext());
    DeviceInfo info = phoneUtils.getDeviceInfo();
    String network_id = phoneUtils.getNetwork();
    String[] sizeParameters = sizeData.sizeDataToJSON(network_id, info.deviceId);

    try {
      for (String parameter : sizeParameters) {
        Logger.w("Uploading RRC size data: " + parameter);
        String response = checkin.serviceRequest("rrc/uploadRRCInferenceSizes", parameter);
        Logger.w("Response from GAE: " + response);
      }
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }

  /**
   * Send the RRC data to the server.
   * 
   * Sent as a separate call because the data is formatted in a different, more complicated way than
   * other measurement tasks.
   * 
   * @param data Contains data to upload
   * @throws IOException
   */
  private void uploadRrcInferenceData(RRCTestData data) throws IOException {
    PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
    Checkin checkin = new Checkin(PhoneUtils.getGlobalContext());
    DeviceInfo info = phoneUtils.getDeviceInfo();
    String network_id = phoneUtils.getNetwork();
    String[] parameters = data.toJSON(network_id, info.deviceId);
    try {
      for (String parameter : parameters) {
        Logger.w("Uploading RRC raw data: " + parameter);
        String response = checkin.serviceRequest("rrc/uploadRRCInference", parameter);
        Logger.w("Response from GAE: " + response);

        Logger.i("TaskSchedule.uploadMeasurementResult() complete");
      }
    } catch (IOException e) {
      throw new IOException(e.getMessage());
    } catch (NumberFormatException e) {
      e.printStackTrace();
    }
  }



  /**
   * Determines the packet size dependence of the RRC task.
   * 
   * Given a specified list of inter-packet intervals, perform the RRC inference measurement for
   * packets of sizes incremented by the size granularity specified.
   * 
   * @param times Inter-packet intervals to test
   * @param desc Parameters for the RRC inference task
   * @param data Stores results of the RRC inference task
   * @param testId A unique ID identifying this set of tests
   */
  private void runSizeThresholdTest(final Integer[] times, RRCDesc desc, RRCTestData data,
      long testId) throws MeasurementError {

    InetAddress serverAddr;
    try {
      serverAddr = InetAddress.getByName(desc.echoHost);
    } catch (UnknownHostException e) {
      Logger.e("Invalid or unreachable echo host. Test aborted.");
      e.printStackTrace();
      return;
    }
    for (int i = 0; i < times.length; i++) {
      for (int j = desc.sizeGranularity; j <= 1024; j += desc.sizeGranularity) {
        // Sometimes the network can change in the middle of a test
        try {
          checkIfWifi();
        } catch (MeasurementError e) {
          throw new MeasurementError("Rescheduled");
        }
        if (stopFlag) {
          throw new MeasurementError("Cancelled");
        }
        
        try {
          long result = inferDemotionPacketSize(serverAddr, times[i], desc, j);
          data.setRrcSizeTestData(times[i], j, result, testId);
        } catch (IOException e) {
          e.printStackTrace();
        } catch (InterruptedException e) {
          e.printStackTrace();
        } catch (MeasurementError e) {
          e.printStackTrace();
        }
      }
    }
  }

  /**
   * Due to problems with how we detect interfering traffic, this test does not always work and
   * results should be treated with caution. I am keeping this test around, though, as we can still
   * get data on how much HTTP handshakes vary in how long they take.
   * 
   * The "times" are the inter-packet intervals at which to run the test. Ideally these should be
   * based on the model constructed by the server, a default assumed value is used in their absence.
   * 
   * Based on the time it takes to load a response from the page.
   * 
   * This test is not currently as accurate as the other tests, for reasons described below, and has
   * thus been disabled.
   * 
   * @param times List of inter-packet intervals, in half-second increments, at which to run the
   *        tests
   * @param desc Stores parameters for the RRC inference tests in general
   * @throws MeasurementError
   */
  private void runHTTPTest(final Integer[] times, RRCDesc desc) throws MeasurementError {
    /*
     * Length of time it takes to request and read in a page.
     */

    Logger.d("Active inference HTTP test: about to begin");
    if (times.length != desc.httpTest.length) {
      desc.httpTest = new int[times.length];
    }
    long startTime = 0;
    long endTime = 0;
    try {
      for (int i = 0; i < times.length; i++) {
        // We try until we reach a threshhold or until there is no
        // competing traffic.
        for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) {
          // Sometimes the network can change in the middle of a test
          try {
            checkIfWifi();
          } catch (MeasurementError e) {
            throw new MeasurementError("Rescheduled");
          }
          if (stopFlag) {
            throw new MeasurementError("Cancelled");
          }

          /*
           * We keep track of the packets sent at the beginning and end of the test so we can detect
           * if there is competing traffic anywhere on the phone.
           */
          PacketMonitor packetMonitor = new PacketMonitor();

          // Initiate the desired RRC state by sending a large enough packet
          // to go to DCH and waiting for the specified amount of time
          try {
            InetAddress serverAddr;
            serverAddr = InetAddress.getByName(desc.echoHost);
            sendPacket(serverAddr, desc.MAX, desc);

            waitTime(times[i] * desc.GRANULARITY, true);

          } catch (InterruptedException e1) {
            e1.printStackTrace();
            continue;
          } catch (UnknownHostException e) {
            e.printStackTrace();
            continue;
          } catch (IOException e) {
            e.printStackTrace();
            continue;
          }
          
          long currentRxTx=Util.getCurrentRxTxBytes();
          startTime = System.currentTimeMillis();
          HttpClient client = new DefaultHttpClient();
          HttpGet request = new HttpGet();

          request.setURI(new URI("http://" + desc.target + "?dummy=" + i + "" + j));

          HttpResponse response = client.execute(request);
          endTime = System.currentTimeMillis();

          BufferedReader in = null;
          in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
          StringBuffer sb = new StringBuffer("");
          String line = "";

          while ((line = in.readLine()) != null) {
            sb.append(line + "\n");
          }
          in.close();
          
          //This may overestimate the data consumed, but there's no good way
          // to tell what was us and what was another app
          incrementData(Util.getCurrentRxTxBytes()-currentRxTx);
          
          
          // not really accurate, just rules out the worst cases of interference
          if (!packetMonitor.isTrafficInterfering(100, 100)) {
            break;
          }
          startTime = 0;
          endTime = 0;

        }

        long rtt = endTime - startTime;
        try {
          desc.setHttp(i, (int) rtt);
        } catch (MeasurementError e) {
          e.printStackTrace();
        }
        Logger.d("Time for Http" + rtt);
      }
    } catch (ClientProtocolException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    } catch (URISyntaxException e) {
      e.printStackTrace();
    }
  }

  /**
   * The "times" are the inter-packet intervals at which to run the test. Ideally these should be
   * based on the model constructed by the server, a default assumed value is used in their absence.
   * 
   * <ol>
   * <li>vSend a packet to initiate the RRC state desired.</li>
   * <li>Create a randomly generated host name (to ensure that the host name is not cached). I found
   * on some devices that even when you clear the cache manually, the data remains in the cache.</li>
   * <li>Time how long it took to look it up.</li>
   * <li>Count the total packets sent, globally on the phone. If more packets were sent than</li>
   * expected, abort and try again. </li>
   * <li>Otherwise, save the data for that test and move to the next inter-packet interval.</li>
   * </ol>
   * 
   * Test is similar to the approach taken in DnsLookUpTask.java.
   * 
   * @param times List of inter-packet intervals, in half-second increments, at which to run the
   *        test
   * @param desc Stores parameters for the RRC inference tests in general
   * @throws MeasurementError
   */

  public void runDnsTest(final Integer[] times, RRCDesc desc) throws MeasurementError {
    Logger.d("Active inference DNS test: about to begin");
    if (times.length != desc.dnsTest.length) {
      desc.dnsTest = new int[times.length];
    }
    long dataConsumedThisTask = 0;
    
    long startTime = 0;
    long endTime = 0;

    // For each inter-packet interval...
    for (int i = 0; i < times.length; i++) {
      // On a failure, try again until a threshold is reached.
      for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) {

        // Sometimes the network can change in the middle of a test
        try {
          checkIfWifi();
        } catch (MeasurementError e) {
          throw new MeasurementError("Rescheduled");
        }
        if (stopFlag) {
          throw new MeasurementError("Cancelled");
        }

        /*
         * We keep track of the packets sent at the beginning and end of the test so we can detect
         * if there is competing traffic anywhere on the phone.
         */

        PacketMonitor packetMonitor = new PacketMonitor();


        // Initiate the desired RRC state by sending a large enough packet
        // to go to DCH and waiting for the specified amount of time
        try {
          InetAddress serverAddr;
          serverAddr = InetAddress.getByName(desc.echoHost);
          sendPacket(serverAddr, desc.MAX, desc);
          waitTime(times[i] * desc.GRANULARITY, true);
        } catch (InterruptedException e1) {
          e1.printStackTrace();
          continue;
        } catch (UnknownHostException e) {
          e.printStackTrace();
          continue;
        } catch (IOException e) {
          e.printStackTrace();
          continue;
        }

        // Create a random URL, to avoid the caching problem
        UUID uuid = UUID.randomUUID();
        String host = uuid.toString() + ".com";
        // Start measuring the time to complete the task
        startTime = System.currentTimeMillis();
        try {
          @SuppressWarnings("unused")
          InetAddress serverAddr = InetAddress.getByName(host);
        } catch (UnknownHostException e) {
          // we do this on purpose! Since it's a fake URL the lookup will fail
        }
        // When we fail to find the URL, we stop timing
        endTime = System.currentTimeMillis();
        
        dataConsumedThisTask += DnsLookupTask.AVG_DATA_USAGE_BYTE;

        // Check how many packets were sent again. If the expected number
        // of packets were sent, we can finish and go to the next task.
        // Otherwise, we have to try again.
        if (!packetMonitor.isTrafficInterfering(5, 5)) {
          break;
        }

        startTime = 0;
        endTime = 0;
      }

      // If we broke out of the try-again loop, the last set of results are
      // valid and we can save them.
      long rtt = endTime - startTime;
      try {
        desc.setDns(i, (int) rtt);
      } catch (MeasurementError e) {
        e.printStackTrace();
      }
      Logger.d("Time for DNS" + rtt);
    }
    incrementData(dataConsumedThisTask);
  }

  /**
   * Time how long it takes to do a TCP 3-way handshake, starting from the induced RRC state.
   * 
   * <ol>
   * <li>Send a packet to initiate the RRC state desired.</li>
   * <li>Open a TCP connection to the echo host server.</li>
   * <li>Time how long it took to look it up.</li>
   * <li>Count the total packets sent, globally on the phone. If more packets were sent than
   * expected, abort and try again.</li>
   * <li>Otherwise, save the data for that test and move to the next inter-packet interval.
   * </ol>
   * 
   * @param times List of inter-packet intervals, in half-second increments, at which to run the
   *        test
   * @param desc Stores parameters for the RRC inference tests in general
   * @throws MeasurementError
   */
  public void runTCPHandshakeTest(final Integer[] times, RRCDesc desc) throws MeasurementError {
    Logger.d("Active inference TCP test: about to begin");
    if (times.length != desc.tcpTest.length) {
      desc.tcpTest = new int[times.length];
    }
    long startTime = 0;
    long endTime = 0;
    long dataConsumedThisTask = 0;

    try {
      // For each inter-packet interval...
      for (int i = 0; i < times.length; i++) {
        // On a failure, try again until a threshhold is reached.
        for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) {
          try {
            checkIfWifi();
          } catch (MeasurementError e) {
            throw new MeasurementError("Rescheduled");
          }
          if (stopFlag) {
            throw new MeasurementError("Cancelled");
          }

          PacketMonitor packetMonitor = new PacketMonitor();

          // Induce DCH then wait for specified time
          InetAddress serverAddr;
          serverAddr = InetAddress.getByName(desc.echoHost);
          sendPacket(serverAddr, desc.MAX, desc);
          waitTime(times[i] * 500, true);

          // begin test. We test the time to do a 3-way handshake only.
          startTime = System.currentTimeMillis();

          serverAddr = InetAddress.getByName(desc.target);
          // three-way handshake done when socket created
          Socket socket = new Socket(serverAddr, 80);
          endTime = System.currentTimeMillis();
          
          // Not exact, but also a smallish task...
          dataConsumedThisTask += DnsLookupTask.AVG_DATA_USAGE_BYTE;


          // Check how many packets were sent again. If the expected number
          // of packets were sent, we can finish and go to the next task.
          // Otherwise, we have to try again.
          if (!packetMonitor.isTrafficInterfering(5, 4)) {
            socket.close();
            break;
          }
          startTime = 0;
          endTime = 0;
          socket.close();
        }
        long rtt = endTime - startTime;
        try {
          desc.setTcp(i, (int) rtt);
        } catch (MeasurementError e) {
          e.printStackTrace();
        }
        Logger.d("Time for TCP" + rtt);
      }
    } catch (InterruptedException e1) {
      e1.printStackTrace();
    } catch (UnknownHostException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
    incrementData(dataConsumedThisTask);
  }

  /**
   * 
   * For all time intervals specified, go through and perform the RRC inference test.
   * 
   * The way this test works, at a high level, is:
   * <ol>
   * <li>Send a large packet to induce DCH (or the equivalent)</li>
   * <li>Wait for an amount of time, t</li>
   * <li>Send a large packet and measure the round-trip time</li>
   * <li>Wait for another amount of time, t</li>
   * <li>Send a small packet and measure the round-trip time 6. Repeat for all specified values of
   * t. These start at 0 and increase by GRANULARITY, "size" times.</li>
   * </ol>
   * 
   * The size of "small" and "large" packets are defined in the parameters. We observe the total
   * packets sent to make sure there is no interfering traffic.
   * 
   * Packets are UDP packets.
   * 
   * From this, we can infer the timers associated with RRC states. By sending a large packet, we
   * induce the highest power state. Waiting a number of seconds afterwards allows us to demote to
   * the next state. Sending a packet and observing the RTT allows us to infer if a state promotion
   * had to take place.
   * 
   * FACH is characterized by different state promotion times for large and small packets.
   * 
   * @param serverAddr Address of the echo server
   * @param desc Stores the parameters for the RRC tests
   * @param data Stores the results of the RRC tests
   * @param utils For fetching the signal strength when the test is performed
   * @return The parameters for the RRC tests
   * @throws InterruptedException
   * @throws IOException
   * @throws MeasurementError
   */
  private RRCDesc inferDemotion(InetAddress serverAddr, RRCDesc desc, RRCTestData data,
      PhoneUtils utils) throws InterruptedException, IOException, MeasurementError {
    Logger.d("Demotion basic test");

    for (int i = 0; i <= desc.size; i++) {

      try {
        checkIfWifi();
      } catch (MeasurementError e) {
        throw new MeasurementError("Rescheduled");
      }
      if (stopFlag) {
        throw new MeasurementError("Cancelled");
      }
      inferDemotionHelper(serverAddr, i, data, desc, utils);
      Logger.d("Finished demotion test with length" + i);

      // Note that we scale from 0-90 to save some stuff for upper layer tests.
      // If we wanted to really do this properly we could scale
      // according to how long each task should take.
    }
    return desc;
  }

  @Override
  public String toString() {
    RRCDesc desc = (RRCDesc) measurementDesc;
    return "[RRC]\n  Echo Server: " + desc.echoHost + "\n  Target: " + desc.target
        + "\n  Interval (sec): " + desc.intervalSec + "\n  Next run: " + desc.startTime;
  }

  /*********************************************************************
   * UTILITIES *
   *********************************************************************/

  /**
   * 
   * Sleep for the amount of time indicated.
   * 
   * @param timeToSleep Time for which we pause the current thread
   * @param useMs Toggles between units of milliseconds for the first parameter (true) and
   *        seconds(false).
   * @throws InterruptedException
   */
  public static void waitTime(int timeToSleep, boolean useMs) throws InterruptedException {

    if (!useMs) {
      Logger.d("Wait for n s: " + timeToSleep);
      timeToSleep = timeToSleep * 1000;
    }
    else {
      Logger.d("Wait for n ms: " + timeToSleep);
    }
    Thread.sleep(timeToSleep);
  }

  /**
   * Sends a bunch of UDP packets of the size indicated and wait for the response.
   * 
   * Counts how long it takes for all the packets to return. PAckets are currently not labelled: the
   * total time is the time for the first packet to leave until the last packet arrives. AFter 7000
   * ms it is assumed packets are lost and the socket times out. In that case, the number of packets
   * lost is recorded.
   * 
   * @param serverAddr server to which to send the packets
   * @param size size of the packets
   * @param num number of packets to send
   * @param packetSize size of the packets sent
   * @param port port to send the packets to
   * @return first value: the amount of time to send all packets and get a response. second value:
   *         number of packets lost, on a timeout.
   * @throws IOException
   */
  public static long[] sendMultiPackets(InetAddress serverAddr, int size, int num, int packetSize,
      int port) throws IOException {

    long startTime = 0;
    long endTime = 0;
    byte[] buf = new byte[size];
    byte[] rcvBuf = new byte[packetSize];
    long[] retval = {-1, -1};
    long numLost = 0;
    int i = 0;
    long dataConsumedThisTask = 0;

    DatagramSocket socket = new DatagramSocket();
    DatagramPacket packetRcv = new DatagramPacket(rcvBuf, rcvBuf.length);
    DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddr, port);
    
    // number * (packet sent + packet received)
    dataConsumedThisTask += num * (size + packetSize);

    try {
      socket.setSoTimeout(7000);
      startTime = System.currentTimeMillis();
      Logger.d("Sending packet, waiting for response ");
      for (i = 0; i < num; i++) {
        socket.send(packet);
      }
      for (i = 0; i < num; i++) {
        socket.receive(packetRcv);
        if (i == 0) {

          endTime = System.currentTimeMillis();
        }
      }
    } catch (SocketTimeoutException e) {
      Logger.d("Timed out");
      numLost += (num - i);
      socket.close();
    }
    Logger.d("Sending complete: " + endTime);

    retval[0] = endTime - startTime;
    retval[1] = numLost;

    incrementData(dataConsumedThisTask);
    return retval;
  }

  /**
   * Helper function that sends a single packet and receives an empty packet back.
   * 
   * @param serverAddr Echo server to calculate round trip
   * @param size size of packet to send in bytes
   * @param desc Holds parameters for the RRC inference task
   * @return The round trip time for the packet
   * @throws IOException
   */
  private static long sendPacket(InetAddress serverAddr, int size, RRCDesc desc) throws IOException {
    return sendPacket(serverAddr, size, desc.MIN, desc.port);
  }

  /**
   * Send a single packet of the size indicated and wait for a response.
   * 
   * After 7000 ms, time out and return a value of -1 (meaning no response). Otherwise, return the
   * time from when the packet was sent to when a response was returned by the echo server.
   * 
   * @param serverAddr Echo server to calculate round trip
   * @param size size of packet to send in bytes
   * @param rcvSize size of packets sent from the echo server
   * @param port where the echo server is listening
   * @return The round trip time for the packet
   * @throws IOException
   */
  public static long sendPacket(InetAddress serverAddr, int size, int rcvSize, int port)
      throws IOException {
    long startTime = 0;
    byte[] buf = new byte[size];
    byte[] rcvBuf = new byte[rcvSize];
    long dataConsumedThisTask = 0;

    DatagramSocket socket = new DatagramSocket();
    DatagramPacket packetRcv = new DatagramPacket(rcvBuf, rcvBuf.length);
    
    dataConsumedThisTask += (size + rcvSize);

    DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddr, port);

    try {
      socket.setSoTimeout(7000);
      startTime = System.currentTimeMillis();
      Logger.d("Sending packet, waiting for response ");

      socket.send(packet);
      socket.receive(packetRcv);
    } catch (SocketTimeoutException e) {
      Logger.d("Timed out, trying again");
      socket.close();
      return -1;
    }
    long endTime = System.currentTimeMillis();
    Logger.d("Sending complete: " + endTime);
    incrementData(dataConsumedThisTask);
    return endTime - startTime;
  }

  /**
   * Performs a single RRC inference test to account for packet sizes.
   * 
   * Sends a packet, waits for the specified length of time, then sends a cluster of packets of the
   * specified size.
   * 
   * @param serverAddr Echo server to calculate round trip
   * @param wait Time to wait between packets, in milliseconds.
   * @param desc Holds parameters for the RRC inference task
   * @param size Size, in bytes, of the packet to send.
   * @return The amount of time to send all packets and get a response.
   * @throws IOException
   * @throws InterruptedException
   */
  public static long inferDemotionPacketSize(InetAddress serverAddr, int wait, RRCDesc desc,
      int size) throws IOException, InterruptedException {
    long retval = -1;
    for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) {
      Logger.d("Active inference: determine packet size, size " + size + " interval " + wait);

      // Induce the highest power state
      sendPacket(serverAddr, desc.MAX, desc.MIN, desc.port);

      // WAit for the specified amount of time
      waitTime(wait * desc.GRANULARITY, true);

      // Send the specified packet size
      long[] rtts = sendMultiPackets(serverAddr, size, 1, desc.MIN, desc.port);
      long rttPacket = rtts[0];

      PacketMonitor packetMonitor = new PacketMonitor();
      if (!packetMonitor.isTrafficInterfering(3, 3)) {
        retval = rttPacket;
        break;
      }
    }
    return retval;
  }

  private long[] inferDemotionHelper(InetAddress serverAddr, int wait, RRCTestData data,
      RRCDesc desc, PhoneUtils utils) throws IOException, InterruptedException {
    return inferDemotionHelper(serverAddr, wait, data, desc, wait, utils);
  }

  /**
   * One component of the RRC inference task.
   * 
   * <ol>
   * <li>Induce the highest-power RRC state by sending a large packet.</li>
   * <li>Wait the indicated number of seconds.</li>
   * <li>Send a series of 10 large packets at once. Measure: a) Time for all packets to be echoed
   * back b) number of packets lost, if any c) associated signal strength d) error rate is currently
   * not implemented.</li>
   * <li>Check if the expected number of packets were sent while performing a test. If too many
   * packets were sent, abort.</li>
   * </ol>
   * 
   * @param serverAddr Echo server to calculate round trip
   * @param wait Time in milliseconds to pause between packets sent
   * @param data Stores the results of the RRC inference tests
   * @param desc Stores parameters for the RRC inference tests
   * @param index Index of the current test, corresponds to the inter-packet time in intervals of
   *        half milliseconds
   * @param utils Used to retrieve the phone's RSSI at the time of collecting the data
   * @return first value: the amount of time to send all small packets and get a response. Second
   *         value: time to send all large packets and get a response.
   * @throws IOException
   * @throws InterruptedException
   */
  public static long[] inferDemotionHelper(InetAddress serverAddr, int wait, RRCTestData data,
      RRCDesc desc, int index, PhoneUtils utils) throws IOException, InterruptedException {
    /**
     * Once we generalize the RRC state inference problem, this is what we will use (since in
     * general, RRC state can differ between large and small packets). Gives the RTT for a large
     * packet and a small packet for a given time after inducing DCH state. Granularity currently
     * half-seconds but can easily be increased.
     * 
     * Measures packets sent before and after to make sure no extra packets were sent, and retries
     * on a failure. Also checks that there was no timeout and retries on a failure.
     */
    long rttLargePacket = -1;
    long rttSmallPacket = -1;
    int packetsLostSmall = 0;
    int packetsLostLarge = 0;

    int errorCountLarge = 0;
    int errorCountSmall = 0;
    int signalStrengthLarge = 0;
    int signalStrengthSmall = 0;

    for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) {
      Logger.d("Active inference: about to begin helper");

      PacketMonitor packetMonitor = new PacketMonitor();

      // Induce the highest power state
      sendPacket(serverAddr, desc.MAX, desc.MIN, desc.port);

      // WAit for the specified amount of time
      waitTime(wait * desc.GRANULARITY, true);

      // Send a bunch of large packets, all at once, and take measurements on the result
//      signalStrengthLarge = utils.getCurrentRssi();
      long[] retval = sendMultiPackets(serverAddr, desc.MAX, 10, desc.MIN, desc.port);
      packetsLostSmall = (int) retval[1];
      rttLargePacket = retval[0];

      // wait for the specified amount of time
      waitTime(wait * desc.GRANULARITY, true);

      // Send a bunch of small packets, all at once, and take measurements on the result
//      signalStrengthSmall = utils.getCurrentRssi();
      retval = sendMultiPackets(serverAddr, desc.MIN, 10, desc.MIN, desc.port);
      packetsLostLarge = (int) retval[1];
      rttSmallPacket = retval[0];

      if (!packetMonitor.isTrafficInterfering(21, 21)) {
        break;
      }
      Logger.d("Try again.");
      rttLargePacket = -1;
      rttSmallPacket = -1;
      packetsLostSmall = 0;
      packetsLostLarge = 0;

      errorCountLarge = 0;
      errorCountSmall = 0;
      signalStrengthLarge = 0;
      signalStrengthSmall = 0;
    }

    Logger.d("3G demotion, lower bound: rtts are:" + rttLargePacket + " " + rttSmallPacket + " "
        + packetsLostSmall + " " + packetsLostLarge);

    long[] retval = {rttLargePacket, rttSmallPacket};
    data.updateAll(index, (int) rttLargePacket, (int) rttSmallPacket, packetsLostSmall,
        packetsLostLarge, errorCountLarge, errorCountSmall, signalStrengthLarge,
        signalStrengthSmall);

    return retval;
  }

  /**
   * Keep a global counter that labels each test with a unique, increasing integer.
   * 
   * @param context Any context instance, needed to fetch the last test id from permanent storage
   * @return The unique (for this device) test ID generated.
   */
  public static synchronized int getTestId(Context context) {
    SharedPreferences prefs = context.getSharedPreferences("test_ids", Context.MODE_PRIVATE);
    int testid = prefs.getInt("test_id", 0) + 1;
    SharedPreferences.Editor editor = prefs.edit();
    editor.putInt("test_id", testid);
    editor.commit();
    return testid;
  }

  /**
   * If on wifi, suspend the task until we go back to the cellular network. Use exponential back-off
   * to calculate the wait time, with a limit of 500 s. Once the limit is reached, unpause other
   * tasks. Repause traffic if we are good to resume again.
   * 
   * @throws MeasurementError
   * 
   */
  public void checkIfWifi() throws MeasurementError {
    PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();

    int timeToWait = 10;
    while (true) {
      if (phoneUtils.getNetwork() != PhoneUtils.NETWORK_WIFI) {
        // RRCTrafficControl.PauseTraffic();
        return;
      }
      if (stopFlag) {
        throw new MeasurementError("Cancelled");
      }

      // if (timeToWait < 60) {
      // RRCTrafficControl.UnPauseTraffic();
      // }
      Logger.d("RRCTask: on Wifi, try again later: " + phoneUtils.getNetwork());
      if (timeToWait < 500) { // 500s, or a bit over 8 minutes.
        timeToWait = timeToWait * 2;
      } else {
        // if it's taking a while, give up this RRC test
        Logger.e("Still not on cellular, timeout after backoff to 500s");
        throw new MeasurementError("Still not on cellular, timeout after backoff to 500s");
      }
      try {
        waitTime(timeToWait, false);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  @Override
  public long getDuration() {
    return this.duration;
  }

  @Override
  public void setDuration(long newDuration) {
    if (newDuration < 0) {
      this.duration = 0;
    } else {
      this.duration = newDuration;
    }
  }
  private synchronized static void incrementData(long data_increment) {
    data_consumed += data_increment;
}

/**  
 * For RRC inference, we calculate this precisely based on the number and size
 * of packets sent.  For the TCP handshake and DNS tasks, we use small, fixed 
 * values based on the average data consumption measured for those tasks.
 * 
 * For the HTTP task, we count <i>all</i> data sent during the task time towards
 * the budget.  This will tend to overestimate the data used, but due to 
 * retransmissions, etc, it is impossible to get a remotely accurate estimate 
 * otherwise, I found.
 */
  @Override
  public long getDataConsumed() {
    return data_consumed;
  }
}