com.mobilyzer.MeasurementScheduler.java Source code

Java tutorial

Introduction

Here is the source code for com.mobilyzer.MeasurementScheduler.java

Source

/*
 * 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;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Calendar;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.PriorityBlockingQueue;

import org.json.JSONException;
import org.json.JSONObject;

import com.mobilyzer.Config;
import com.mobilyzer.MeasurementTask;
import com.mobilyzer.UpdateIntent;
import com.mobilyzer.gcm.GCMManager;
import com.mobilyzer.measurements.RRCTask;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.PhoneUtils;
import com.mobilyzer.util.MeasurementJsonConvertor;

import android.app.AlarmManager;
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.os.IBinder;
import android.os.Messenger;
import android.os.Parcelable;
import android.preference.PreferenceManager;

/**
 * 
 * @author Ashkan Nikravesh (ashnik@umich.edu) + others The single scheduler thread that monitors
 *         the task queue, runs tasks at their specified times, and finally retrieves and reports
 *         results once they finish. The API can call the public methods or this Service. This
 *         service works as a remote service and always, we will have a single instance of scheduler
 *         running on a device, although we can have more than one app that binds to this service
 *         and communicate with that.
 */
public class MeasurementScheduler extends Service {

    public enum TaskStatus {
        FINISHED, PAUSED, CANCELLED, SCHEDULED, RUNNING, NOTFOUND
    }

    public enum DataUsageProfile {
        PROFILE1, PROFILE2, PROFILE3, PROFILE4, UNLIMITED, NOTASSIGNED
    }

    private ExecutorService measurementExecutor;
    private BroadcastReceiver broadcastReceiver;
    public boolean isSchedulerStarted = false;

    public Checkin checkin;// TODO: should not be public, quick fix to get gcm working
    private long checkinIntervalSec;
    private long checkinRetryIntervalSec;
    private int checkinRetryCnt;
    private CheckinTask checkinTask;
    private Calendar lastCheckinTime;

    private int batteryThreshold;
    // private int dataLimit;//in Byte
    private DataUsageProfile dataUsageProfile;

    private PhoneUtils phoneUtils;

    private PendingIntent measurementIntentSender;
    private PendingIntent checkinIntentSender;
    private PendingIntent checkinRetryIntentSender;

    private volatile HashMap<String, Date> serverTasks;
    // private volatile HashMap <String, MeasurementTask> currentSchedule;

    private AlarmManager alarmManager;
    private ResourceCapManager resourceCapManager;
    private volatile ConcurrentHashMap<String, TaskStatus> tasksStatus;
    // all the tasks are put in to this queue first, where they ordered based on their start time
    private volatile PriorityBlockingQueue<MeasurementTask> mainQueue;
    // ready queue, all the tasks in this queue are ready to be run. They sorted based on
    // (1) priority (2) end time
    private volatile PriorityBlockingQueue<MeasurementTask> waitingTasksQueue;
    private volatile ConcurrentHashMap<MeasurementTask, Future<MeasurementResult[]>> pendingTasks;
    private volatile Date currentTaskStartTime;
    private volatile MeasurementTask currentTask;

    private volatile ConcurrentHashMap<String, String> idToClientKey;

    private Messenger messenger;

    private GCMManager gcmManager;

    @Override
    public void onCreate() {
        Logger.d("MeasurementScheduler -> onCreate called");
        PhoneUtils.setGlobalContext(this.getApplicationContext());

        phoneUtils = PhoneUtils.getPhoneUtils();
        phoneUtils.registerSignalStrengthListener();

        this.measurementExecutor = Executors.newSingleThreadExecutor();
        this.mainQueue = new PriorityBlockingQueue<MeasurementTask>(Config.MAX_TASK_QUEUE_SIZE,
                new TaskComparator());
        this.waitingTasksQueue = new PriorityBlockingQueue<MeasurementTask>(Config.MAX_TASK_QUEUE_SIZE,
                new WaitingTasksComparator());
        this.pendingTasks = new ConcurrentHashMap<MeasurementTask, Future<MeasurementResult[]>>();
        this.tasksStatus = new ConcurrentHashMap<String, MeasurementScheduler.TaskStatus>();
        this.alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
        this.resourceCapManager = new ResourceCapManager(Config.DEFAULT_BATTERY_THRESH_PRECENT, this);

        this.serverTasks = new HashMap<String, Date>();
        // this.currentSchedule = new HashMap<String, MeasurementTask>();

        this.idToClientKey = new ConcurrentHashMap<String, String>();

        messenger = new Messenger(new APIRequestHandler(this));

        gcmManager = new GCMManager(this.getApplicationContext());

        this.setCurrentTask(null);
        this.setCurrentTaskStartTime(null);

        this.checkin = new Checkin(this);
        this.checkinRetryIntervalSec = Config.MIN_CHECKIN_RETRY_INTERVAL_SEC;
        this.checkinRetryCnt = 0;
        this.checkinTask = new CheckinTask();

        this.batteryThreshold = -1;
        this.checkinIntervalSec = -1;
        this.dataUsageProfile = DataUsageProfile.NOTASSIGNED;

        //    loadSchedulerState();//TODO(ASHKAN)

        // Register activity specific BroadcastReceiver here
        IntentFilter filter = new IntentFilter();
        filter.addAction(UpdateIntent.CHECKIN_ACTION);
        filter.addAction(UpdateIntent.CHECKIN_RETRY_ACTION);
        filter.addAction(UpdateIntent.MEASUREMENT_ACTION);
        filter.addAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION);
        filter.addAction(UpdateIntent.GCM_MEASUREMENT_ACTION);
        filter.addAction(UpdateIntent.PLT_MEASUREMENT_ACTION);

        broadcastReceiver = new BroadcastReceiver() {

            @Override
            public void onReceive(Context context, Intent intent) {
                Logger.d(intent.getAction() + " RECEIVED");
                if (intent.getAction().equals(UpdateIntent.MEASUREMENT_ACTION)) {
                    handleMeasurement();

                } else if (intent.getAction().equals(UpdateIntent.GCM_MEASUREMENT_ACTION)) {
                    try {
                        JSONObject json = new JSONObject(
                                intent.getExtras().getString(UpdateIntent.MEASUREMENT_TASK_PAYLOAD));
                        Logger.d("MeasurementScheduler -> GCMManager: json task Value is " + json);
                        if (json != null && MeasurementTask.getMeasurementTypes().contains(json.get("type"))) {

                            try {
                                MeasurementTask task = MeasurementJsonConvertor.makeMeasurementTaskFromJson(json);
                                task.getDescription().priority = MeasurementTask.GCM_PRIORITY;
                                task.getDescription().startTime = new Date(System.currentTimeMillis() - 1000);
                                task.getDescription().endTime = new Date(System.currentTimeMillis() + (600 * 1000));
                                task.generateTaskID();
                                task.getDescription().key = Config.SERVER_TASK_CLIENT_KEY;
                                submitTask(task);
                            } catch (IllegalArgumentException e) {
                                Logger.w("MeasurementScheduler -> GCM : Could not create task from JSON: " + e);
                            }
                        }

                    } catch (JSONException e) {
                        Logger.e(
                                "MeasurementSchedule -> GCMManager : Got exception during converting GCM json to MeasurementTask",
                                e);
                    }

                } else if (intent.getAction().equals(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION)) {
                    String taskid = intent.getStringExtra(UpdateIntent.TASKID_PAYLOAD);
                    String taskKey = intent.getStringExtra(UpdateIntent.CLIENTKEY_PAYLOAD);
                    int priority = intent.getIntExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD,
                            MeasurementTask.INVALID_PRIORITY);

                    Logger.e(
                            intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD) + " " + taskid + " " + taskKey);
                    if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals(Config.TASK_FINISHED)) {
                        tasksStatus.put(taskid, TaskStatus.FINISHED);
                        Parcelable[] results = intent.getParcelableArrayExtra(UpdateIntent.RESULT_PAYLOAD);
                        if (results != null) {
                            sendResultToClient(results, priority, taskKey, taskid);

                            for (Object obj : results) {
                                try {
                                    MeasurementResult result = (MeasurementResult) obj;
                                    /**
                                     * Nullify the additional parameters in MeasurmentDesc, or the results won't be
                                     * accepted by GAE server
                                     */
                                    result.getMeasurementDesc().parameters = null;
                                    String jsonResult = MeasurementJsonConvertor.encodeToJson(result).toString();
                                    saveResultToFile(jsonResult);
                                } catch (JSONException e) {
                                    Logger.e("Error converting results to json format", e);
                                }
                            }
                        }
                        handleMeasurement();
                    } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals(Config.TASK_PAUSED)) {
                        tasksStatus.put(taskid, TaskStatus.PAUSED);
                    } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD)
                            .equals(Config.TASK_STOPPED)) {
                        tasksStatus.put(taskid, TaskStatus.SCHEDULED);
                    } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD)
                            .equals(Config.TASK_CANCELED)) {
                        tasksStatus.put(taskid, TaskStatus.CANCELLED);
                        Parcelable[] results = intent.getParcelableArrayExtra(UpdateIntent.RESULT_PAYLOAD);
                        if (results != null) {
                            sendResultToClient(results, priority, taskKey, taskid);
                        }
                    } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD)
                            .equals(Config.TASK_STARTED)) {
                        tasksStatus.put(taskid, TaskStatus.RUNNING);
                    } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD)
                            .equals(Config.TASK_RESUMED)) {
                        tasksStatus.put(taskid, TaskStatus.RUNNING);
                    }
                } else if (intent.getAction().equals(UpdateIntent.CHECKIN_ACTION)
                        || intent.getAction().equals(UpdateIntent.CHECKIN_RETRY_ACTION)) {
                    Logger.d("Checkin intent received");
                    handleCheckin();
                }
            }
        };
        this.registerReceiver(broadcastReceiver, filter);
    }

    /**
     * Save the results of a task to a file, for later uploading. This way, if the application
     * crashes, is halted, etc. between the task and checkin, no results are lost.
     * 
     * @param result The JSON representation of a result, as a string
     */
    private synchronized void saveResultToFile(String result) {
        try {
            Logger.i("Saving result to file...");
            BufferedOutputStream writer = new BufferedOutputStream(
                    openFileOutput("results", Context.MODE_PRIVATE | Context.MODE_APPEND));
            result += "\n";
            Logger.d("Measurement size in byte: " + result.length());
            writer.write(result.getBytes());
            writer.close();
        } catch (FileNotFoundException e) {
            Logger.e("saveResultToFile->", e);
        } catch (IOException e) {
            Logger.e("saveResultToFile->", e);
        }
    }

    /*
     * This callback ensures that we always use newest version scheduler on the device
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            Logger.e("MeasurementScheduler -> onStartCommand. Get start service intent from "
                    + intent.getStringExtra(UpdateIntent.CLIENTKEY_PAYLOAD) + " (API Ver "
                    + intent.getStringExtra(UpdateIntent.VERSION_PAYLOAD) + ")");
            /**
             * Fetch the version in startService intent and stop itself if the version in the intent is
             * higher than its own.
             */
            int currentAPIVersion = Integer.parseInt(Config.version);
            int newAPIVersion = Integer.parseInt(intent.getStringExtra(UpdateIntent.VERSION_PAYLOAD));

            gcmManager.checkPlayServices();
            if (currentAPIVersion < newAPIVersion) {
                Logger.e("Found scheduler version " + newAPIVersion + ", current version " + currentAPIVersion);
                Logger.e("Scheduler " + android.os.Process.myPid() + " stop itself");
                this.stopScheduler();
                return START_STICKY;
            }
        }
        // Start up the thread running the service.
        // Using one single thread for all requests
        if (isSchedulerStarted == false) {
            Logger.i("starting scheduler, load parameters from preference");
            isSchedulerStarted = true;
            loadFromPreference();
        }
        return START_STICKY;
    }

    /**
     * Load setting parameters from shared preference
     */
    private void loadFromPreference() {
        Logger.d("Scheduler loadFromPreference called");
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        boolean isForced = (PhoneUtils.clientKeySet.size() == 1);

        String currentBatteryThreshold = prefs.getString(Config.PREF_KEY_BATTERY_THRESHOLD,
                String.valueOf(Config.DEFAULT_BATTERY_THRESH_PRECENT));
        setBatteryThresh(isForced, Integer.parseInt(currentBatteryThreshold));

        // API will always call startService before binding. So it is
        // safe to set checkin interval here
        String currentCheckinInterval = prefs.getString(Config.PREF_KEY_CHECKIN_INTERVAL,
                String.valueOf(Config.DEFAULT_CHECKIN_INTERVAL_SEC));
        setCheckinInterval(false, Long.parseLong(currentCheckinInterval));

        // Fetch the data limit with 250 MB as a default
        String currentProfile = prefs.getString(Config.PREF_KEY_DATA_USAGE_PROFILE,
                DataUsageProfile.PROFILE3.name());
        this.setDataUsageLimit(isForced, DataUsageProfile.valueOf(currentProfile));

        Logger.i("Preference set from SharedPreference: CheckinInterval=" + this.checkinIntervalSec
                + ", Battery Threshold= " + this.batteryThreshold + ", Profile=" + this.dataUsageProfile.name());
    }

    @Override
    public IBinder onBind(Intent intent) {
        return messenger.getBinder();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        // All clients unbind by calling unbindService(). We can safely stop scheduler
        Logger.e("All Clients unbind. Safe to stop now");
        this.stopScheduler();
        return false;
    }

    @Override
    public void onDestroy() {
        Logger.d("MeasurementScheduler -> onDestroy");
        super.onDestroy();
        cleanUp();
    }

    // return the current running task
    public synchronized MeasurementTask getCurrentTask() {
        return currentTask;
    }

    // set current running task
    public synchronized void setCurrentTask(MeasurementTask newTask) {
        if (newTask == null) {
            currentTask = null;
            Logger.d("Setting Current task -> null");
        } else {
            Logger.d("Setting Current task: " + newTask.getTaskId());
            currentTask = newTask.clone();
        }

    }

    // set current running task start time
    private synchronized void setCurrentTaskStartTime(Date starttime) {
        currentTaskStartTime = starttime;
    }

    // return the current running task (TODO synchronized?)
    private synchronized Date getCurrentTaskStartTime() {
        Date starttime;
        starttime = currentTaskStartTime;
        return starttime;
    }

    private synchronized void handleMeasurement() {
        alarmManager.cancel(measurementIntentSender);
        setCurrentTask(null);
        try {
            Logger.d("MeasurementScheduler -> In handleMeasurement " + mainQueue.size() + " "
                    + waitingTasksQueue.size());
            MeasurementTask task = mainQueue.peek();
            // update the waiting queue. It contains all the tasks that are ready
            // to be executed. Here we extract all those ready tasks from main queue
            while (task != null && task.timeFromExecution() <= 0) {
                mainQueue.poll();

                if (task.getDescription().getType().equals(RRCTask.TYPE)
                        && phoneUtils.getNetwork().equals(PhoneUtils.NETWORK_WIFI)) {
                    long updatedStartTime = System.currentTimeMillis() + (long) (10 * 60 * 1000);
                    task.getDescription().startTime.setTime(updatedStartTime);
                    mainQueue.add(task);
                    Logger.i("MeasurementScheduler: handleMeasurement: delaying RRC task on "
                            + phoneUtils.getNetwork());
                    task = mainQueue.peek();
                    continue;
                }
                Logger.i("MeasurementScheduler: handleMeasurement: " + task.getDescription().key + " "
                        + task.getDescription().type + " added to waiting list");
                waitingTasksQueue.add(task);
                task = mainQueue.peek();
            }

            if (!phoneUtils.isNetworkAvailable()) {
                Logger.i("No connection is available, set an alarm for 5 min");
                measurementIntentSender = PendingIntent.getBroadcast(this, 0,
                        new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION), PendingIntent.FLAG_CANCEL_CURRENT);
                alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (5 * 60 * 1000),
                        measurementIntentSender);
                return;
            }

            if (waitingTasksQueue.size() != 0) {
                Logger.i("waiting list size is " + waitingTasksQueue.size());
                MeasurementTask ready = waitingTasksQueue.poll();
                Logger.i("ready: " + ready.getDescription().getType());

                MeasurementDesc desc = ready.getDescription();
                long newStartTime = desc.startTime.getTime() + (long) desc.intervalSec * 1000;

                if (desc.count == MeasurementTask.INFINITE_COUNT
                        && desc.priority != MeasurementTask.USER_PRIORITY) {
                    if (serverTasks.containsKey(desc.toString())
                            && serverTasks.get(desc.toString()).after(desc.endTime)) {
                        ready.getDescription().endTime.setTime(serverTasks.get(desc.toString()).getTime());
                    }
                }

                /**
                 * Add a clone of the task if it's still valid it does not change the taskID (hashCode)
                 */
                if (newStartTime < ready.getDescription().endTime.getTime()
                        && (desc.count == MeasurementTask.INFINITE_COUNT || desc.count > 1)) {
                    MeasurementTask newTask = ready.clone();
                    if (desc.count != MeasurementTask.INFINITE_COUNT) {
                        newTask.getDescription().count--;
                    }
                    newTask.getDescription().startTime.setTime(newStartTime);
                    tasksStatus.put(newTask.getTaskId(), TaskStatus.SCHEDULED);
                    mainQueue.add(newTask);
                } else {
                    if (desc.priority != MeasurementTask.USER_PRIORITY) {
                        serverTasks.remove(desc.toString());
                    }
                }

                if (ready.getDescription().endTime.before(new Date())) {
                    Intent intent = new Intent();
                    intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION);
                    intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_CANCELED);
                    MeasurementResult[] tempResults = MeasurementResult.getFailureResult(ready,
                            new CancellationException("Task cancelled!"));
                    intent.putExtra(UpdateIntent.RESULT_PAYLOAD, tempResults);
                    intent.putExtra(UpdateIntent.TASKID_PAYLOAD, ready.getTaskId());
                    intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, ready.getKey());
                    MeasurementScheduler.this.sendBroadcast(intent);

                    if (desc.priority != MeasurementTask.USER_PRIORITY) {
                        serverTasks.remove(desc.toString());
                    }

                    handleMeasurement();
                } else {
                    Logger.d("MeasurementScheduler -> " + ready.getDescription().getType() + " is gonna run");
                    Future<MeasurementResult[]> future;
                    setCurrentTask(ready);
                    setCurrentTaskStartTime(Calendar.getInstance().getTime());
                    if (ready.getDescription().priority == MeasurementTask.USER_PRIORITY) {
                        // User task can override the power policy. So a different task wrapper is used.
                        future = measurementExecutor.submit(new UserMeasurementTask(ready, this));
                    } else {
                        future = measurementExecutor
                                .submit(new ServerMeasurementTask(ready, this, resourceCapManager));
                    }

                    synchronized (pendingTasks) {
                        pendingTasks.put(ready, future);
                    }
                }
            } else {// if(task.timeFromExecution()>0){
                MeasurementTask waiting = mainQueue.peek();
                if (waiting != null) {
                    long timeFromExecution = task.timeFromExecution();
                    measurementIntentSender = PendingIntent.getBroadcast(this, 0,
                            new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION), PendingIntent.FLAG_CANCEL_CURRENT);
                    alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeFromExecution,
                            measurementIntentSender);
                    setCurrentTask(null);// TODO
                    setCurrentTaskStartTime(null);
                }

            }
        } catch (IllegalArgumentException e) {
            // Task creation in clone can create this exception

        } catch (Exception e) {
            // We don't want any unexpected exception to crash the process
        }
    }

    // returns taskId on success submissions
    public synchronized String submitTask(MeasurementTask newTask) {
        // TODO check if scheduler is running...
        // and there is a current running/scheduled task
        String newTaskId = newTask.getTaskId();
        tasksStatus.put(newTaskId, TaskStatus.SCHEDULED);
        idToClientKey.put(newTaskId, newTask.getKey());
        Logger.d("MeasurementScheduler --> submitTask: " + newTask.getDescription().key + " " + newTaskId);
        MeasurementTask current;
        if (getCurrentTask() != null) {
            current = getCurrentTask();
            Logger.d("submitTask: current is NOT null");
        } else {
            current = null;
            Logger.d("submitTask: current is null");
        }
        // preemption condition
        if (current != null && newTask.getDescription().priority < current.getDescription().priority
                && new Date(current.getDuration() + getCurrentTaskStartTime().getTime())
                        .after(newTask.getDescription().endTime)) {
            Logger.d("submitTask: trying to cancel/preempt the task");
            // finding the current instance in pending tasks. we can call
            // pause on that instance only
            if (pendingTasks.containsKey(current)) {
                for (MeasurementTask mt : pendingTasks.keySet()) {
                    if (current.equals(mt)) {
                        current = mt;
                        break;
                    }
                }
                Logger.e("Cancelling Current Task");
                if (current instanceof PreemptibleMeasurementTask
                        && ((PreemptibleMeasurementTask) current).pause()) {
                    pendingTasks.remove(current);
                    ((PreemptibleMeasurementTask) current).updateTotalRunningTime(
                            System.currentTimeMillis() - getCurrentTaskStartTime().getTime());
                    if (newTask.timeFromExecution() <= 0) {
                        mainQueue.add(newTask);
                        mainQueue.add(current);
                        handleMeasurement();
                    } else {
                        mainQueue.add(newTask);
                        mainQueue.add(current);
                        long timeFromExecution = newTask.timeFromExecution();
                        measurementIntentSender = PendingIntent.getBroadcast(this, 0,
                                new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION),
                                PendingIntent.FLAG_CANCEL_CURRENT);
                        alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeFromExecution,
                                measurementIntentSender);
                        setCurrentTask(newTask);
                        setCurrentTaskStartTime(new Date(System.currentTimeMillis() + timeFromExecution));
                    }

                } else if (current.stop()) {
                    pendingTasks.remove(current);
                    if (newTask.timeFromExecution() <= 0) {
                        mainQueue.add(newTask);
                        mainQueue.add(current);
                        handleMeasurement();
                    } else {
                        mainQueue.add(newTask);
                        mainQueue.add(current);
                        long timeFromExecution = newTask.timeFromExecution();
                        measurementIntentSender = PendingIntent.getBroadcast(this, 0,
                                new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION),
                                PendingIntent.FLAG_CANCEL_CURRENT);
                        alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeFromExecution,
                                measurementIntentSender);
                        // setCurrentTask(null);
                        setCurrentTask(newTask);
                        setCurrentTaskStartTime(new Date(System.currentTimeMillis() + timeFromExecution));
                    }
                } else {
                    mainQueue.add(newTask);
                }
            } else {
                alarmManager.cancel(measurementIntentSender);
                if (newTask.timeFromExecution() <= 0) {
                    mainQueue.add(newTask);
                    handleMeasurement();
                } else {
                    mainQueue.add(newTask);
                    long timeFromExecution = newTask.timeFromExecution();
                    measurementIntentSender = PendingIntent.getBroadcast(this, 0,
                            new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION), PendingIntent.FLAG_CANCEL_CURRENT);
                    alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeFromExecution,
                            measurementIntentSender);
                    // setCurrentTask(null);
                    setCurrentTask(newTask);
                    setCurrentTaskStartTime(new Date(System.currentTimeMillis() + timeFromExecution));
                }
            }
        } else {
            Logger.d("submitTask: adding to mainqueue");
            mainQueue.add(newTask);
            if (current == null) {
                Logger.d("submitTask: adding to mainqueue, current is null");
                Logger.d("submitTask: calling handleMeasurement");
                alarmManager.cancel(measurementIntentSender);
                handleMeasurement();
            } else {
                Logger.d("submitTask: adding to mainqueue, current is not null: " + current.getMeasurementType()
                        + " " + getCurrentTaskStartTime());
                if (pendingTasks.containsKey(current)) {
                    Logger.d("submitTask: isDone?" + pendingTasks.get(current).isDone());
                    if (pendingTasks.get(current).isDone()) {
                        alarmManager.cancel(measurementIntentSender);
                        alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 3 * 1000,
                                measurementIntentSender);
                    } else if (getCurrentTaskStartTime() != null) {
                        if (!current.getMeasurementType().equals(RRCTask.TYPE)
                                && new Date(System.currentTimeMillis() - Config.MAX_TASK_DURATION)
                                        .after(getCurrentTaskStartTime())) {
                            Logger.d("submitTask: 1");
                            pendingTasks.get(current).cancel(true);
                            handleMeasurement();

                        } else if (current.getMeasurementType().equals(RRCTask.TYPE) && new Date(
                                System.currentTimeMillis() - (Config.DEFAULT_RRC_TASK_DURATION + 15 * 60 * 1000))
                                        .after(getCurrentTaskStartTime())) {
                            Logger.d("submitTask: 2");
                            pendingTasks.get(current).cancel(true);
                            handleMeasurement();
                        } else {
                            Logger.d("submitTask: 3");
                            alarmManager.set(AlarmManager.RTC_WAKEUP,
                                    System.currentTimeMillis() + Config.MAX_TASK_DURATION / 2,
                                    measurementIntentSender);
                        }
                    }

                } else {
                    Logger.d("submitTask: not found in pending task");
                    handleMeasurement();
                }

            }
        }
        return newTaskId;
    }

    public synchronized boolean cancelTask(String taskId, String clientKey) {

        if (clientKey.equals(Config.SERVER_TASK_CLIENT_KEY)) {
            return false;
        }

        Logger.i("Cancel task " + taskId + " from " + clientKey);
        if (taskId != null && idToClientKey.containsKey(taskId)) {
            if (idToClientKey.get(taskId).equals(clientKey)) {
                boolean found = false;
                for (Object object : mainQueue) {
                    MeasurementTask task = (MeasurementTask) object;
                    if (task.getTaskId().equals(taskId) && task.getKey().equals(clientKey)) {
                        mainQueue.remove(task);
                        found = true;
                    }
                }

                for (Object object : waitingTasksQueue) {
                    MeasurementTask task = (MeasurementTask) object;
                    if (task.getTaskId().equals(taskId) && task.getKey().equals(clientKey)) {
                        waitingTasksQueue.remove(task);
                        found = true;
                    }
                }
                MeasurementTask currentMeasurementTask = getCurrentTask();
                if (currentMeasurementTask != null) {
                    Logger.i("submitTask: current taskId " + currentMeasurementTask.getTaskId());
                    if (currentMeasurementTask.getTaskId().equals(taskId)
                            && currentMeasurementTask.getKey().equals(clientKey)) {
                        for (MeasurementTask mt : pendingTasks.keySet()) {
                            if (currentMeasurementTask.equals(mt)) {
                                currentMeasurementTask = mt;
                                break;
                            }
                        }
                        pendingTasks.remove(currentMeasurementTask);
                        return currentMeasurementTask.stop();
                    }
                }

                return found;
            }
        }
        return false;
    }

    private void persistParams(String key, String value) {
        Logger.d("Scheduler persistParams called");
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString(key, value);
        editor.commit();
    }

    /**
     * @param newBatteryThresh
     * @return the final value of battery threshold
     */
    // API should ensure that the value is between 0 and 100
    public synchronized int setBatteryThresh(boolean isForced, int newBatteryThresh) {
        Logger.d("Scheduler->setBatteryThresh called");
        int min = Config.MIN_BATTERY_THRESHOLD;
        int max = Config.MAX_BATTERY_THRESHOLD;
        /**
         * If there are multiple apps registered, we gonna be conservative Only allow raising the
         * battery threshold
         */
        if (!isForced && this.batteryThreshold != -1) {
            min = this.batteryThreshold;
        }
        // Check whether out of boundary and same with old value
        if (newBatteryThresh >= min && newBatteryThresh <= max && newBatteryThresh != this.batteryThreshold) {
            this.batteryThreshold = newBatteryThresh;
            Logger.i("Setting battery threshold to " + this.batteryThreshold + "%");
            persistParams(Config.PREF_KEY_BATTERY_THRESHOLD, String.valueOf(this.batteryThreshold));
        }
        this.resourceCapManager.setBatteryThresh(this.batteryThreshold);
        return this.batteryThreshold;
    }

    /**
     * If the users have not specified the threshold, it will return the default value
     * 
     * @return
     */
    public synchronized int getBatteryThresh() {
        if (this.batteryThreshold == -1) {
            return Config.DEFAULT_BATTERY_THRESH_PRECENT;
        }
        return this.batteryThreshold;
    }

    /** Set the interval for checkin in seconds */
    public synchronized long setCheckinInterval(boolean isForced, long interval) {
        Logger.d("Scheduler->setCheckinInterval called " + interval);
        long min = Config.MIN_CHECKIN_INTERVAL_SEC;
        long max = Config.MAX_CHECKIN_INTERVAL_SEC;

        if (!isForced && this.checkinIntervalSec != -1) {
            min = this.batteryThreshold;
        }
        // Check whether out of boundary and same with old value
        if (interval >= min && interval <= max && interval != this.checkinIntervalSec) {
            this.checkinIntervalSec = interval;
            Logger.i("Setting checkin interval to " + this.checkinIntervalSec + " seconds");
            persistParams(Config.PREF_KEY_CHECKIN_INTERVAL, String.valueOf(this.checkinIntervalSec));
            // the new checkin schedule will start
            // in PAUSE_BETWEEN_CHECKIN_CHANGE_MSEC seconds
            checkinIntentSender = PendingIntent.getBroadcast(this, 0, new UpdateIntent(UpdateIntent.CHECKIN_ACTION),
                    PendingIntent.FLAG_CANCEL_CURRENT);
            alarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
                    System.currentTimeMillis() + Config.PAUSE_BETWEEN_CHECKIN_CHANGE_MSEC,
                    checkinIntervalSec * 1000, checkinIntentSender);
        }
        return this.checkinIntervalSec;
    }

    /** Returns the checkin interval of the scheduler in seconds */
    public synchronized long getCheckinInterval() {
        if (this.checkinIntervalSec == -1) {
            return Config.DEFAULT_CHECKIN_INTERVAL_SEC;
        }
        return this.checkinIntervalSec;
    }

    public synchronized void setDataUsageLimit(boolean isForced, DataUsageProfile profile) {
        Logger.d("Scheduler->setDataUsageLimit called");
        int min = DataUsageProfile.PROFILE1.ordinal();
        int max = DataUsageProfile.UNLIMITED.ordinal();
        if (!isForced && profile != null && profile != DataUsageProfile.NOTASSIGNED) {
            max = this.dataUsageProfile.ordinal();
        }
        if (profile.ordinal() >= min && profile.ordinal() <= max && profile != this.dataUsageProfile) {
            this.dataUsageProfile = profile;
            resourceCapManager.setDataUsageLimit(profile);
            Logger.i("Setting data usage profile to " + this.dataUsageProfile);
            persistParams(Config.PREF_KEY_DATA_USAGE_PROFILE, this.dataUsageProfile.name());
        }
    }

    public synchronized DataUsageProfile getDataUsageProfile() {
        if (this.dataUsageProfile.equals(DataUsageProfile.NOTASSIGNED)) {
            return DataUsageProfile.PROFILE3;
        }
        return this.dataUsageProfile;
    }

    public synchronized void setAuthenticateAccount(String selectedAccount) {
        Logger.d("Scheduler->setAuthenticateAccount called");
        if (selectedAccount != null) {
            // Verify that the account is valid or is anonymous
            String[] accounts = AccountSelector.getAccountList(getApplicationContext());
            for (String account : accounts) {
                if (account.equals(selectedAccount)) {
                    Logger.i("Setting authenticate account to " + account);
                    persistParams(Config.PREF_KEY_SELECTED_ACCOUNT, account);
                    return;
                }
            }
            Logger.e("Error: " + selectedAccount + " doesn't match any account or anonymous");
        }
    }

    /**
     * Get current authenticate account of Mobilyzer
     * 
     * @return account in string if already set by clients(include Anonymous), or null if havn't been
     *         set yet(it will checkin as anonymous user)
     */
    public synchronized String getAuthenticateAccount() {
        Logger.d("Scheduler->getAuthenticateAccount called");
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        String selectedAccount = prefs.getString(Config.PREF_KEY_SELECTED_ACCOUNT, null);
        Logger.i("Get authenticate account: " + selectedAccount);
        return selectedAccount;
    }

    /**
     * Adjusts the frequency of the task based on the profile passed from the server.
     * 
     * Alternately, disregards the task altogether, if a -1 is passed.
     * 
     * @param task The task to adjust
     * @return false if the task should not be scheduled, based on the profile
     */
    private boolean adjustInterval(MeasurementTask task) {

        Map<String, String> params = task.getDescription().parameters;
        float adjust = 1; // default
        if (params.containsKey("profile_1_freq") && getDataUsageProfile() == DataUsageProfile.PROFILE1) {
            adjust = Float.parseFloat(params.get("profile_1_freq"));
            Logger.i("Task " + task.getDescription().key + " adjusted using profile 1");
        } else if (params.containsKey("profile_2_freq") && getDataUsageProfile() == DataUsageProfile.PROFILE2) {
            adjust = Float.parseFloat(params.get("profile_2_freq"));
            Logger.i("Task " + task.getDescription().key + " adjusted using profile 2");
        } else if (params.containsKey("profile_3_freq") && getDataUsageProfile() == DataUsageProfile.PROFILE3) {
            adjust = Float.parseFloat(params.get("profile_3_freq"));
            Logger.i("Task " + task.getDescription().key + " adjusted using profile 3");
        } else if (params.containsKey("profile_4_freq") && getDataUsageProfile() == DataUsageProfile.PROFILE4) {
            adjust = Float.parseFloat(params.get("profile_4_freq"));
            Logger.i("Task " + task.getDescription().key + " adjusted using profile 4");
        } else if (params.containsKey("profile_unlimited") && getDataUsageProfile() == DataUsageProfile.UNLIMITED) {
            adjust = Float.parseFloat(params.get("profile_unlimited"));
            Logger.i("Task " + task.getDescription().key + " adjusted using unlimited profile");
        }
        if (adjust <= 0) {
            Logger.i("Task " + task.getDescription().key + "marked for removal");
            return false;
        }
        // if (!task.getType().equals(RRCTask.TYPE)) {
        // return false;
        // }
        task.getDescription().intervalSec *= adjust;
        Calendar now = Calendar.getInstance();
        now.add(Calendar.SECOND, (int) (task.getDescription().intervalSec / 24));
        task.getDescription().startTime = now.getTime();
        if (task.getDescription().startTime.after(task.getDescription().endTime)) {
            task.getDescription().endTime = new Date(
                    task.getDescription().startTime.getTime() + Config.TASK_EXPIRATION_MSEC);
        }
        // Logger.d(task.getDescription().startTime+" "+task.getDescription().endTime);
        return true;

    }

    public TaskStatus getTaskStatus(String taskID) {
        if (tasksStatus.get(taskID) == null) {
            return TaskStatus.NOTFOUND;
        }
        return tasksStatus.get(taskID);
    }

    private class TaskComparator implements Comparator<MeasurementTask> {

        @Override
        public int compare(MeasurementTask task1, MeasurementTask task2) {
            return task1.compareTo(task2);
        }
    }

    private class WaitingTasksComparator implements Comparator<MeasurementTask> {

        @Override
        public int compare(MeasurementTask task1, MeasurementTask task2) {
            Long task1Prority = task1.measurementDesc.priority;
            Long task2Priority = task2.measurementDesc.priority;

            int priorityComparison = task1Prority.compareTo(task2Priority);
            if (priorityComparison == 0 && task1.measurementDesc.endTime != null
                    && task2.measurementDesc.endTime != null) {
                return task1.measurementDesc.endTime.compareTo(task2.measurementDesc.endTime);
            } else {
                return priorityComparison;
            }
        }
    }

    /**
     * Send measurement results to the client that submit the task, or broadcast the result of server
     * scheduled task to each connected clients
     * 
     * @param results Measurement result to be sent
     * @param priority Priority for the task. Determine the communication way - Unicast for user task,
     *        Broadcast for server task
     * @param clientKey Client key for the task
     * @param taskId Unique task id for the task
     */
    public void sendResultToClient(Parcelable[] results, int priority, String clientKey, String taskId) {
        Intent intent = new Intent();
        intent.putExtra(UpdateIntent.RESULT_PAYLOAD, results);
        intent.putExtra(UpdateIntent.TASKID_PAYLOAD, taskId);
        if (priority == MeasurementTask.USER_PRIORITY) {
            Logger.d("Sending result to client " + clientKey + ": taskId " + taskId);
            intent.setAction(UpdateIntent.USER_RESULT_ACTION + "." + clientKey);
        } else {
            Logger.d("Broadcasting result: taskId " + taskId);
            intent.setAction(UpdateIntent.SERVER_RESULT_ACTION);
        }
        this.sendBroadcast(intent);
    }

    /**
     * Stop the scheduler
     */
    public synchronized void stopScheduler() {
        this.notifyAll();
        // this.stopForeground(true);
        this.stopSelf();
    }

    private synchronized void cleanUp() {
        Logger.d("Service cleanUp called");
        this.mainQueue.clear();
        this.waitingTasksQueue.clear();

        if (this.currentTask != null) {
            this.currentTask.stop();
        }

        // remove all future tasks
        this.measurementExecutor.shutdown();
        // remove and stop all active tasks
        this.measurementExecutor.shutdownNow();
        this.checkin.shutDown();

        this.unregisterReceiver(broadcastReceiver);
        Logger.d("canceling pending intents");

        if (checkinIntentSender != null) {
            checkinIntentSender.cancel();
            alarmManager.cancel(checkinIntentSender);
        }
        if (checkinRetryIntentSender != null) {
            checkinRetryIntentSender.cancel();
            alarmManager.cancel(checkinRetryIntentSender);
        }
        if (measurementIntentSender != null) {
            measurementIntentSender.cancel();
            alarmManager.cancel(measurementIntentSender);
        }
        this.notifyAll();
        phoneUtils.shutDown();

        Logger.i("Shut down all executors and stopping service");
    }

    private void getTasksFromServer() throws IOException {
        Logger.i("Downloading tasks from the server");
        // checkin.getCookie();
        List<MeasurementTask> tasksFromServer = checkin.checkin(resourceCapManager, gcmManager);
        // The new task schedule overrides the old one

        Logger.i("Received " + tasksFromServer.size() + " task(s) from server");

        for (MeasurementTask task : tasksFromServer) {
            task.measurementDesc.key = Config.SERVER_TASK_CLIENT_KEY;
            if (adjustInterval(task)) {
                if (task.getDescription().count == MeasurementTask.INFINITE_COUNT) {
                    if (!serverTasks.containsKey(task.getDescription().toString())) {
                        this.mainQueue.add(task);
                    }
                    serverTasks.put(task.getDescription().toString(), task.getDescription().endTime);
                } else {
                    this.mainQueue.add(task);
                }
            }
        }
        //    saveSchedulerState();//TODO(ASHKAN)
    }

    /**
     * Save the entire current schedule to a file, in JSON format, like how tasks are received from
     * the server.
     * 
     * One item per line.
     */
    private void saveSchedulerState() {

        try {
            BufferedOutputStream writer = new BufferedOutputStream(
                    openFileOutput("schedule", Context.MODE_PRIVATE));
            Object[] mainQueueArray = new Object[0];
            Logger.i("Saving schedule to a file...");
            synchronized (mainQueue) {
                mainQueueArray = mainQueue.toArray();
            }

            for (Object entry : mainQueueArray) {
                try {
                    JSONObject task = MeasurementJsonConvertor
                            .encodeToJson(((MeasurementTask) entry).getDescription());
                    String taskstring = task.toString() + "\n";
                    writer.write(taskstring.getBytes());
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }

            Object[] waitingTasksArray = new Object[0];
            synchronized (pendingTasks) {
                waitingTasksArray = waitingTasksQueue.toArray();
            }

            for (Object entry : waitingTasksArray) {
                try {
                    JSONObject task = MeasurementJsonConvertor
                            .encodeToJson(((MeasurementTask) entry).getDescription());
                    String taskstring = task.toString() + "\n";
                    writer.write(taskstring.getBytes());
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            writer.close();
        } catch (FileNotFoundException e) {
            Logger.e("saveSchedulerState->", e);
        } catch (IOException e) {
            Logger.e("saveSchedulerState->", e);
        }

    }

    /**
     * Load the schedule from the schedule file, if it exists.
     * 
     * This is to be run when the app first starts up, so scheduled items are not lost.
     */
    private void loadSchedulerState() {
        // Vector<MeasurementTask> tasksToAdd = new Vector<MeasurementTask>();
        synchronized (mainQueue) {
            try {
                Logger.i("Restoring schedule from disk...");
                FileInputStream inputstream = openFileInput("schedule");
                InputStreamReader streamreader = new InputStreamReader(inputstream);
                BufferedReader bufferedreader = new BufferedReader(streamreader);

                String line;
                while ((line = bufferedreader.readLine()) != null) {
                    JSONObject jsonTask;
                    try {
                        jsonTask = new JSONObject(line);
                        MeasurementTask newTask = MeasurementJsonConvertor.makeMeasurementTaskFromJson(jsonTask);

                        // If the task is scheduled in the past, re-schedule it in the future
                        // We assume tasks in the past have run, otherwise we can wind up getting
                        // stuck trying to run a large backlog of tasks

                        long curtime = System.currentTimeMillis();

                        if (curtime > newTask.getDescription().endTime.getTime()) {
                            continue;
                        }

                        if (curtime > newTask.getDescription().startTime.getTime()) {
                            long timediff = curtime - newTask.getDescription().startTime.getTime();

                            timediff = (long) (timediff % (newTask.getDescription().intervalSec * 1000));
                            Calendar now = Calendar.getInstance();
                            now.add(Calendar.SECOND, (int) timediff / 1000);
                            newTask.getDescription().startTime.setTime(now.getTimeInMillis());
                            Logger.i("Rescheduled task " + newTask.getDescription().key + " at time "
                                    + now.getTimeInMillis());
                        }

                        mainQueue.add(newTask);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
                handleMeasurement();
                bufferedreader.close();
                streamreader.close();
                inputstream.close();

            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    private void resetCheckin() {
        // reset counters for checkin
        checkinRetryCnt = 0;
        checkinRetryIntervalSec = Config.MIN_CHECKIN_RETRY_INTERVAL_SEC;
        checkin.initializeAccountSelector();
    }

    private class CheckinTask implements Runnable {
        @Override
        public void run() {
            Logger.i("checking Speedometer service for new tasks");
            lastCheckinTime = Calendar.getInstance();
            PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
            try {
                uploadResults();
                getTasksFromServer();
                // Also reset checkin if we get a success
                resetCheckin();
                // Schedule the new tasks
                if (getCurrentTask() == null) {// TODO check this
                    alarmManager.cancel(measurementIntentSender);
                    handleMeasurement();
                }
                //
            } catch (Exception e) {
                /*
                 * Executor stops all subsequent execution of a periodic task if a raised exception is
                 * uncaught. We catch all undeclared exceptions here
                 */
                Logger.e("Unexpected exceptions caught", e);
                if (checkinRetryCnt > Config.MAX_CHECKIN_RETRY_COUNT) {
                    /*
                     * If we have retried more than MAX_CHECKIN_RETRY_COUNT times upon a checkin failure, we
                     * will stop retrying and wait until the next checkin period
                     */
                    resetCheckin();
                } else if (checkinRetryIntervalSec < checkinIntervalSec) {
                    Logger.i("Retrying checkin in " + checkinRetryIntervalSec + " seconds");
                    /*
                     * Use checkinRetryIntentSender so that the periodic checkin schedule will remain intact
                     */
                    checkinRetryIntentSender = PendingIntent.getBroadcast(MeasurementScheduler.this, 0,
                            new UpdateIntent(UpdateIntent.CHECKIN_RETRY_ACTION), PendingIntent.FLAG_CANCEL_CURRENT);
                    alarmManager.set(AlarmManager.RTC_WAKEUP,
                            System.currentTimeMillis() + checkinRetryIntervalSec * 1000, checkinRetryIntentSender);
                    checkinRetryCnt++;
                    checkinRetryIntervalSec = Math.min(Config.MAX_CHECKIN_RETRY_INTERVAL_SEC,
                            checkinRetryIntervalSec * 2);
                }
            } finally {
                if (phoneUtils != null) {
                    phoneUtils.releaseWakeLock();
                }
            }
        }
    }

    private void uploadResults() {
        Vector<MeasurementResult> finishedTasks = new Vector<MeasurementResult>();
        MeasurementResult[] results;
        Future<MeasurementResult[]> future;
        Logger.d("pendingTasks: " + pendingTasks.size());
        synchronized (this.pendingTasks) {
            try {
                for (MeasurementTask task : this.pendingTasks.keySet()) {
                    future = this.pendingTasks.get(task);
                    if (future != null) {
                        if (future.isDone()) {
                            this.pendingTasks.remove(task);
                            if (future.isCancelled()) {
                                Logger.e("Task execution was canceled");
                                results = MeasurementResult.getFailureResult(task,
                                        new CancellationException("Task cancelled"));
                                for (MeasurementResult r : results) {
                                    finishedTasks.add(r);
                                }
                            }
                        }
                    }
                }
            } catch (ConcurrentModificationException e) {
                /*
                 * keySet is a synchronized view of the keys. However, changes during iteration will throw
                 * ConcurrentModificationException. Since we have synchronized all changes to pendingTasks
                 * this should not happen.
                 */
                Logger.e("Pending tasks is changed during measurement upload");
            }
        }
        Logger.i("A total of " + finishedTasks.size() + " from pendingTasks is uploaded");
        Logger.i("A total of " + this.pendingTasks.size() + " is in pendingTasks");

        try {
            for (MeasurementResult r : finishedTasks) {
                r.getMeasurementDesc().parameters = null;
            }
            this.checkin.uploadMeasurementResult(finishedTasks, resourceCapManager);
        } catch (IOException e) {
            Logger.e("Error when uploading message");
        }
    }

    /** Returns the last checkin time */
    public synchronized Date getLastCheckinTime() {
        if (lastCheckinTime != null) {
            return lastCheckinTime.getTime();
        } else {
            return null;
        }
    }

    /** Returns the next (expected) checkin time */
    public synchronized Date getNextCheckinTime() {
        if (lastCheckinTime != null) {
            Calendar nextCheckinTime = (Calendar) lastCheckinTime.clone();
            nextCheckinTime.add(Calendar.SECOND, (int) getCheckinInterval());
            return nextCheckinTime.getTime();
        } else {
            return null;
        }
    }

    /**
     * Perform a checkin operation.
     */
    public void handleCheckin() {
        // if ( PhoneUtils.getPhoneUtils().isCharging() == false
        // && PhoneUtils.getPhoneUtils().getCurrentBatteryLevel() < getBatteryThresh()) {
        if (!resourceCapManager.canScheduleExperiment()) {
            Logger.e("Checkin skipped - below battery threshold " + getBatteryThresh());
            return;
        }
        /*
         * The CPU can go back to sleep immediately after onReceive() returns. Acquire the wake lock for
         * the new thread here and release the lock when the thread finishes
         */
        PhoneUtils.getPhoneUtils().acquireWakeLock();
        new Thread(checkinTask).start();
    }
}