com.mobiperf.MeasurementScheduler.java Source code

Java tutorial

Introduction

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

Source

/*
 * Copyright 2012 Google Inc.
 * 
 * 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.mobiperf;

import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;

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.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.PriorityBlockingQueue;

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

import com.google.myjson.reflect.TypeToken;
import com.mobiperf.ResourceCapManager.DataUsageProfile;
import com.mobiperf.ResourceCapManager.PowerAwareTask;
import com.mobiperf.util.MeasurementJsonConvertor;
import com.mobiperf.util.PhoneUtils;
import com.mobiperf.R;

/**
 * The single scheduler thread that monitors the task queue, runs tasks at their specified times,
 * and finally retrieves and reports results once they finish.
 * 
 * All method invocations on the singleton object are thread-safe.
 */
public class MeasurementScheduler extends Service {

    // This arbitrary id is private to Speedometer
    private static final int NOTIFICATION_ID = 1234;

    private ExecutorService measurementExecutor;
    private BroadcastReceiver broadcastReceiver;
    private Boolean pauseRequested = true;
    private boolean stopRequested = false;
    private boolean isSchedulerStarted = false;
    private Checkin checkin;
    private long checkinIntervalSec;
    private long checkinRetryIntervalSec;
    private int checkinRetryCnt;
    private CheckinTask checkinTask;
    private Calendar lastCheckinTime;

    private PendingIntent checkinIntentSender;
    /**
     * Intent for checkin retries. Reusing checkinIntentSender for retries will cancel any previously
     * configured periodic checkin schedule. Thus we need a separate intent sender
     */
    private PendingIntent checkinRetryIntentSender;
    private PendingIntent measurementIntentSender;
    private AlarmManager alarmManager;
    private ResourceCapManager resourceCapManager;
    /*
     * Both taskQueue and pendingTasks are thread safe and operations on them are atomic. To guarantee
     * reliable value propagation between threads, use volatile keyword.
     */
    private volatile PriorityBlockingQueue<MeasurementTask> taskQueue;
    private volatile ConcurrentHashMap<MeasurementTask, Future<MeasurementResult>> pendingTasks;
    // Binder given to clients
    private final IBinder binder = new SchedulerBinder();

    private MeasurementTask currentTask;

    private NotificationManager notificationManager;
    private int completedMeasurementCnt = 0;
    private int failedMeasurementCnt = 0;

    private ArrayList<String> userResults;
    private ArrayList<String> systemResults;
    private ArrayList<String> systemConsole;

    // We keep track of the current tasks running, indexed by their unique IDs,
    // for the purpose of selectively updating our schedule when new tasks are
    // received from the server
    private Hashtable<String, MeasurementTask> currentSchedule;

    private PhoneUtils phoneUtils;

    /**
     * The Binder class that returns an instance of running scheduler
     */
    public class SchedulerBinder extends Binder {
        public MeasurementScheduler getService() {
            return MeasurementScheduler.this;
        }
    }

    /*
     * Returns a IBinder that contains the instance of the MeasurementScheduler object
     * 
     * @see android.app.Service#onBind(android.content.Intent)
     */
    @Override
    public IBinder onBind(Intent intent) {
        Logger.d("Service onBind called");
        return this.binder;
    }

    // Service objects are by nature singletons enforced by Android
    @Override
    public void onCreate() {
        Logger.d("Service onCreate called");
        PhoneUtils.setGlobalContext(this.getApplicationContext());
        phoneUtils = PhoneUtils.getPhoneUtils();
        phoneUtils.registerSignalStrengthListener();
        this.checkin = new Checkin(this);
        this.checkinRetryIntervalSec = Config.MIN_CHECKIN_RETRY_INTERVAL_SEC;
        this.checkinRetryCnt = 0;
        this.checkinTask = new CheckinTask();

        this.pauseRequested = true;
        this.stopRequested = false;
        this.measurementExecutor = Executors.newSingleThreadExecutor();
        this.taskQueue = new PriorityBlockingQueue<MeasurementTask>(Config.MAX_TASK_QUEUE_SIZE,
                new TaskComparator());
        this.pendingTasks = new ConcurrentHashMap<MeasurementTask, Future<MeasurementResult>>();

        // expect it to be the same size as the queue
        this.currentSchedule = new Hashtable<String, MeasurementTask>(Config.MAX_TASK_QUEUE_SIZE);

        this.notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        this.alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
        this.resourceCapManager = new ResourceCapManager(Config.DEFAULT_BATTERY_THRESH_PRECENT, this);

        restoreState();

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

        broadcastReceiver = new BroadcastReceiver() {
            // Handles various broadcast intents.

            // If traffic is paused by RRCTrafficControl (because a RRC test is
            // running), we do not perform the checkin, since sending interfering
            // traffic makes the RRC inference task abort and restart the current
            // test as the traffic may have altered the phone's RRC state.
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals(UpdateIntent.PREFERENCE_ACTION)) {
                    updateFromPreference();
                } else if (intent.getAction().equals(UpdateIntent.CHECKIN_ACTION)
                        || intent.getAction().equals(UpdateIntent.CHECKIN_RETRY_ACTION)
                                && !RRCTrafficControl.checkIfPaused()) {
                    Logger.d("Checkin intent received");
                    handleCheckin(false);
                } else if (intent.getAction().equals(UpdateIntent.MEASUREMENT_ACTION)
                        && !RRCTrafficControl.checkIfPaused()) {
                    Logger.d("MeasurementIntent intent received");
                    handleMeasurement();
                } else if (intent.getAction().equals(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION)) {
                    Logger.d("MeasurementIntent update intent received");
                    if (intent.getIntExtra(UpdateIntent.PROGRESS_PAYLOAD,
                            Config.INVALID_PROGRESS) == Config.MEASUREMENT_END_PROGRESS) {
                        if (intent.getStringExtra(UpdateIntent.ERROR_STRING_PAYLOAD) != null) {
                            failedMeasurementCnt++;
                        } else {
                            // Process result
                            completedMeasurementCnt++;
                        }
                        if (intent.getStringExtra(UpdateIntent.RESULT_PAYLOAD) != null) {
                            Logger.d("Measurement result intent received");
                            saveResultToFile(intent.getStringExtra(UpdateIntent.RESULT_PAYLOAD));

                        }
                        updateResultsConsole(intent);
                    }
                } else if (intent.getAction().equals(UpdateIntent.MSG_ACTION)) {
                    String msg = intent.getExtras().getString(UpdateIntent.STRING_PAYLOAD);
                    Date now = Calendar.getInstance().getTime();
                    insertStringToConsole(systemConsole, now + "\n\n" + msg);
                }
            }
        };
        this.registerReceiver(broadcastReceiver, filter);
        // TODO(mdw): Make this a user-selectable option
        addIconToStatusBar();
    }

    public boolean hasBatteryToScheduleExperiment() {
        return resourceCapManager.canScheduleExperiment();
    }

    /**
     * Create notification that indicates the service is running.
     */
    private Notification createServiceRunningNotification() {
        // The intent to launch when the user clicks the expanded notification
        Intent intent = new Intent(this, SpeedometerApp.class);
        PendingIntent pendIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);

        // This constructor is deprecated in 3.x. But most phones still run 2.x systems
        Notification notice = new Notification(R.drawable.icon_statusbar,
                getString(R.string.notificationSchedulerStarted), System.currentTimeMillis());
        notice.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;

        // This is deprecated in 3.x. But most phones still run 2.x systems
        notice.setLatestEventInfo(this, getString(R.string.app_name),
                getString(R.string.notificationServiceRunning), pendIntent);
        return notice;
    }

    /**
     * Add an icon to the device status bar.
     */
    private void addIconToStatusBar() {
        notificationManager.notify(NOTIFICATION_ID, createServiceRunningNotification());
    }

    /**
     * Remove the icon from the device status bar.
     */
    private void removeIconFromStatusBar() {
        notificationManager.cancel(NOTIFICATION_ID);
    }

    /**
     * Keep the service in the foreground, preventing it from being killed in low-memory situations.
     */
    @SuppressWarnings("unused")
    private void startSpeedometerInForeGround() {
        Logger.d("Service startSpeedometerInForeGround called");

        // Put scheduler service into foreground. Makes the process less likely of being killed
        startForeground(NOTIFICATION_ID, createServiceRunningNotification());
    }

    /**
     * Perform a checkin operation.
     */
    public void handleCheckin(boolean force) {
        if (!userConsented()) {
            Logger.i("Skipping checkin - User has not consented");
            return;
        }

        // New addition: check if the RRC task has paused other tasks.
        if ((!force && isPauseRequested()) || RRCTrafficControl.checkIfPaused()) {
            sendStringMsg("Skipping checkin - app is paused");
            return;
        }
        if (!force && !resourceCapManager.canScheduleExperiment()) {
            sendStringMsg("Skipping checkin - below battery threshold");
            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();
    }

    private void handleMeasurement() {
        if (!userConsented()) {
            Logger.i("Skipping measurement - User has not consented");
            return;
        }

        try {
            MeasurementTask task = taskQueue.peek();
            // Process the head of the queue.
            if (task != null && task.timeFromExecution() <= 0) {
                taskQueue.poll();
                Future<MeasurementResult> future;
                Logger.i("Processing task " + task.toString());
                // Run the head task using the executor
                if (task.getDescription().priority == MeasurementTask.USER_PRIORITY) {
                    sendStringMsg("Scheduling user task:\n" + task);
                    // User task can override the power policy. So a different task wrapper is used.
                    future = measurementExecutor.submit(new UserMeasurementTask(task));
                } else {
                    sendStringMsg("Scheduling task:\n" + task);
                    future = measurementExecutor.submit(new PowerAwareTask(task, resourceCapManager, this));
                }
                synchronized (pendingTasks) {
                    pendingTasks.put(task, future);
                }

                MeasurementDesc desc = task.getDescription();

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

                // Add a clone of the task if it's still valid.
                if (newStartTime < desc.endTime.getTime()
                        && (desc.count == MeasurementTask.INFINITE_COUNT || desc.count > 1)) {
                    MeasurementTask newTask = task.clone();
                    if (desc.count != MeasurementTask.INFINITE_COUNT) {
                        newTask.getDescription().count--;
                    }
                    newTask.getDescription().startTime.setTime(newStartTime);
                    submitTask(newTask);
                }
            }
            // Schedule the next measurement in the taskQueue
            task = taskQueue.peek();
            if (task != null) {
                long timeFromExecution = Math.max(task.timeFromExecution(),
                        Config.MIN_TIME_BETWEEN_MEASUREMENT_ALARM_MSEC);
                measurementIntentSender = PendingIntent.getBroadcast(this, 0,
                        new UpdateIntent("", UpdateIntent.MEASUREMENT_ACTION), PendingIntent.FLAG_CANCEL_CURRENT);
                alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeFromExecution,
                        measurementIntentSender);
            }
        } catch (IllegalArgumentException e) {
            // Task creation in clone can create this exception
            Logger.e("Exception when cloning task");
            sendStringMsg("Exception when cloning task: " + e);
        } catch (Exception e) {
            // We don't want any unexpected exception to crash the process
            Logger.e("Exception when handling measurements", e);
            sendStringMsg("Exception running task: " + e);
        }
        persistState();
    }

    /**
     * Sets the current task being run. In the current implementation, the synchronized keyword is not
     * needed because only one thread runs measurements and calls this method. It is not thread safe.
     */
    public void setCurrentTask(MeasurementTask task) {
        this.currentTask = task;
    }

    /**
     * Returns the current task being run. In the current implementation, the synchronized keyword is
     * not needed because only one thread runs measurements and calls this method. It is not thread
     * safe.
     */
    public MeasurementTask getCurrentTask() {
        return this.currentTask;
    }

    /**
     * Removes the first task in the taskQueue with the taskKey
     */
    public boolean removeTaskByKey(String taskKey) {
        Iterator<MeasurementTask> it = taskQueue.iterator();
        while (it.hasNext()) {
            MeasurementTask task = it.next();
            if (task.getDescription().key.equals(taskKey)) {
                it.remove();
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the current task queue in the scheduler.
     */
    public PriorityBlockingQueue<MeasurementTask> getTaskQueue() {
        return taskQueue;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Logger.d("Service onStartCommand called, isSchedulerStarted = " + isSchedulerStarted);
        // Start up the thread running the service. Using one single thread for all requests
        Logger.i("starting scheduler");
        sendStringMsg("Scheduler starting");
        if (!isSchedulerStarted) {
            restoreState();
            updateFromPreference();
            this.resume();
            /*
             * There is no onStop() for services. The service is only stopped when the user exits the
             * application. So don't worry about setting isSchedulerStarted to false.
             */
            isSchedulerStarted = true;
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        Logger.d("Service onDestroy called");
        super.onDestroy();
        cleanUp();
    }

    /**
     * Returns the power manager used by the scheduler
     * */
    public ResourceCapManager resourceCapManager() {
        return this.resourceCapManager;
    }

    /** Set the interval for checkin in seconds */
    public synchronized void setCheckinInterval(long interval) {
        this.checkinIntervalSec = Math.max(Config.MIN_CHECKIN_INTERVAL_SEC, interval);
        // 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);

        Logger.i("Setting checkin interval to " + interval + " seconds");
    }

    /** Returns the checkin interval of the scheduler in seconds */
    public synchronized long getCheckinInterval() {
        return this.checkinIntervalSec;
    }

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

    /**
     * Prevents new tasks from being scheduled. Started task will still run to finish.
     */
    public synchronized void pause() {
        Logger.d("Service pause called");
        sendStringMsg("Scheduler pausing");
        this.pauseRequested = true;
        updateStatus();
    }

    /** Enables new tasks to be scheduled */
    public synchronized void resume() {
        Logger.d("Service resume called");
        sendStringMsg("Scheduler resuming");
        this.pauseRequested = false;
        updateStatus();
    }

    /** Return whether new tasks can be scheduled */
    public synchronized boolean isPauseRequested() {
        return this.pauseRequested;
    }

    /** Remove all tasks that have not been scheduled */
    public synchronized void removeAllUnscheduledTasks() {
        this.taskQueue.clear();
    }

    /** Return the number of tasks that have not been scheduled */
    public int getUnscheduledTaskCount() {
        return this.taskQueue.size();
    }

    /** Return the next task to be scheduled */
    public MeasurementTask getNextTaskToBeScheduled() {
        return this.taskQueue.peek();
    }

    /** Return the number of pending tasks that have been scheduled */
    public int getPendingTaskCount() {
        return this.pendingTasks.size();
    }

    private class TaskComparator implements Comparator<MeasurementTask> {

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

    /** Request the scheduler to stop execution. */
    public synchronized void requestStop() {
        sendStringMsg("Scheduler stop requested");
        this.stopRequested = true;
        this.notifyAll();
        this.stopForeground(true);
        this.removeIconFromStatusBar();
        this.stopSelf();
    }

    /**
     * Submit a MeasurementTask to the scheduler. Caller of this method can broadcast an intent with
     * MEASUREMENT_ACTION to start the measurement immediately.
     */
    public boolean submitTask(MeasurementTask task) {
        try {
            // Immediately handles measurements created by user
            if (task.getDescription().priority == MeasurementTask.USER_PRIORITY) {
                return this.taskQueue.add(task);
            }

            if (taskQueue.size() >= Config.MAX_TASK_QUEUE_SIZE
                    || pendingTasks.size() >= Config.MAX_TASK_QUEUE_SIZE) {
                return false;
            }
            // Automatically notifies the scheduler waiting on taskQueue.take()
            return this.taskQueue.add(task);
        } catch (NullPointerException e) {
            Logger.e("The task to be added is null");
            return false;
        } catch (ClassCastException e) {
            Logger.e("cannot compare this task against existing ones");
            return false;
        }
    }

    @SuppressWarnings("unused")
    private void updateNotificationBar(String notificationMsg) {
        // The intent to launch when the user clicks the expanded notification
        Intent intent = new Intent(this, SpeedometerApp.class);
        PendingIntent pendIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);

        // This constructor is deprecated in 3.x. But most phones still run 2.x systems
        Notification notice = new Notification(R.drawable.icon_statusbar, notificationMsg,
                System.currentTimeMillis());

        // This is deprecated in 3.x. But most phones still run 2.x systems
        notice.setLatestEventInfo(this, "Speedometer", notificationMsg, pendIntent);

        notificationManager.notify(NOTIFICATION_ID, notice);
    }

    /**
     * Broadcast an intent to update the system status.
     */
    public void updateStatus() {
        Intent intent = new Intent();
        intent.setAction(UpdateIntent.SYSTEM_STATUS_UPDATE_ACTION);
        String statsMsg = completedMeasurementCnt + " completed, " + failedMeasurementCnt + " failed";
        intent.putExtra(UpdateIntent.STATS_MSG_PAYLOAD, statsMsg);
        sendBroadcast(intent);
    }

    private void updateFromPreference() {
        Logger.d("Service updateFromPreference called");
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        try {
            resourceCapManager
                    .setBatteryThresh(Integer.parseInt(prefs.getString(getString(R.string.batteryMinThresPrefKey),
                            String.valueOf(Config.DEFAULT_BATTERY_THRESH_PRECENT))));

            // Fetch the data limit with 250 MB as a default
            resourceCapManager.setDataUsageLimit(prefs.getString(getString(R.string.dataLimitPrefKey), "250 MB"));

            this.setCheckinInterval(Integer.parseInt(prefs.getString(getString(R.string.checkinIntervalPrefKey),
                    String.valueOf(Config.DEFAULT_CHECKIN_INTERVAL_SEC / 3600))) * 3600);

            updateStatus();

            Logger.i("Preference set from SharedPreference: " + "checkinInterval=" + checkinIntervalSec
                    + ", minBatThres= " + resourceCapManager.getBatteryThresh());
        } catch (ClassCastException e) {
            Logger.e("exception when casting preference values", e);
        }
    }

    /**
     * Write a string to the system console.
     */
    public void sendStringMsg(String str) {
        UpdateIntent intent = new UpdateIntent(str, UpdateIntent.MSG_ACTION);
        this.sendBroadcast(intent);
    }

    private synchronized void cleanUp() {
        Logger.d("Service cleanUp called");
        this.taskQueue.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);
        }
        persistState();
        this.notifyAll();
        phoneUtils.shutDown();

        removeIconFromStatusBar();

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

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

    private void getTasksFromServer() throws IOException {
        Logger.i("Downloading tasks from the server");
        // Do not download new tasks while the RRC task is running
        // to avoid interference
        if (RRCTrafficControl.checkIfPaused()) {
            return;
        }
        checkin.getCookie();
        List<MeasurementTask> tasksFromServer = checkin.checkin(resourceCapManager);

        updateSchedule(tasksFromServer, false);

    }

    /**
     * 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")
                && resourceCapManager.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")
                && resourceCapManager.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")
                && resourceCapManager.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")
                && resourceCapManager.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")
                && resourceCapManager.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;
        }
        task.getDescription().intervalSec *= adjust;
        task.getDescription().updateStartTime(); // Needed because the start time is set on creation
        return true;

    }

    /**
     * Update the schedule based on a set of tasks from the server.
     * <p>
     * The current tasks to schedule are in a hash table indexed by a unique task key.
     * <p>
     * Remove all tasks from the schedule that are not in the new list or that have changed.
     * Then, add all tasks from the new list that were not in the schedule, or have changed.
     * Then, the schedule will match the one in the server, and unchanged tasks are left as they are.
     * 
     * <p>
     * If the state has changed and the schedule was received from the server, save it to disk
     * so it can be recovered in case of a crash. 
     * 
     * @param newTasks List of MeasurementTasks from the server
     * @param reLoad if it's True, we're loading from disk: don't adjust frequencies or save to disk again.
     */
    private void updateSchedule(List<MeasurementTask> newTasks, boolean reLoad) {

        // Keep track of what tasks need to be added.
        // Altered tasks are removed and then added, so they go here too
        Vector<MeasurementTask> tasksToAdd = new Vector<MeasurementTask>();

        // Keep track of what keys are not being used. Remove keys from this as
        // you find they are in use.
        Set<String> missingKeys = new HashSet<String>(currentSchedule.keySet());
        Set<String> keysToRemove = new HashSet<String>();

        Logger.i("Attempting to add new tasks");

        for (MeasurementTask newTask : newTasks) {

            // Adjust the frequency of the new task, based on the selected data consumption profile,
            // or ignore it if the task is disabled for this profile.
            // If we are loading again, don't re-adjust task frequencies.
            if (!reLoad) {
                if (!adjustInterval(newTask)) {
                    continue;
                }
            }

            String newKey = newTask.getDescription().key;
            if (!missingKeys.contains(newKey)) {
                tasksToAdd.add(newTask);
            } else {
                // check for changes. If any parameter changes, it counts as a change.
                if (!currentSchedule.get(newKey).getDescription().equals(newTask.getDescription())) {
                    // If there's a change, replace the task with the new task from the server
                    keysToRemove.add(newKey);
                    tasksToAdd.add(newTask);
                }
                // We've seen the task
                missingKeys.remove(newKey);
            }
        }

        // scheduleKeys now contain all keys that do not exist
        keysToRemove.addAll(missingKeys);

        // Add all new tasks, and copy all unmodified tasks, to a new queue.
        // Also update currentSchedule accordingly.
        PriorityBlockingQueue<MeasurementTask> newQueue = new PriorityBlockingQueue<MeasurementTask>(
                Config.MAX_TASK_QUEUE_SIZE, new TaskComparator());

        synchronized (currentSchedule) {
            Logger.i("Tasks to remove:" + keysToRemove.size());
            for (MeasurementTask task : this.taskQueue) {
                String taskKey = task.getDescription().key;
                if (!keysToRemove.contains(taskKey)) {
                    newQueue.add(task);
                } else {
                    Logger.w("Removing task with key" + taskKey);
                    // Also need to keep our master schedule up to date
                    currentSchedule.remove(taskKey);
                }
            }
            this.taskQueue = newQueue;
            // add all new tasks
            Logger.i("New tasks added:" + tasksToAdd.size());
            for (MeasurementTask task : tasksToAdd) {
                submitTask(task);
                currentSchedule.put(task.getDescription().key, task);
            }
        }

        if (!reLoad && (!tasksToAdd.isEmpty() || !keysToRemove.isEmpty())) {
            saveSchedulerState();
        }
    }

    /**
     * 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";
            writer.write(result.getBytes());
            writer.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Read in the results of tasks completed to date from a file, then clear the file.
     * 
     * @return The results as a JSONArray, ready for sending to the server.
     */
    private synchronized JSONArray readResultsFromFile() {

        JSONArray results = new JSONArray();
        try {
            Logger.i("Loading results from disk");
            FileInputStream inputstream = openFileInput("results");
            InputStreamReader streamreader = new InputStreamReader(inputstream);
            BufferedReader bufferedreader = new BufferedReader(streamreader);

            String line;
            while ((line = bufferedreader.readLine()) != null) {
                JSONObject jsonTask;
                try {
                    jsonTask = new JSONObject(line);
                    results.put(jsonTask);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }

            bufferedreader.close();
            streamreader.close();
            inputstream.close();

            // delete file once done, to avoid uploading results twice
            deleteFile("results");

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

    /**
     * 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() {
        synchronized (currentSchedule) {
            try {
                BufferedOutputStream writer = new BufferedOutputStream(
                        openFileOutput("schedule", Context.MODE_PRIVATE));

                Logger.i("Saving schedule to a file...");
                for (Map.Entry<String, MeasurementTask> entry : currentSchedule.entrySet()) {
                    try {
                        JSONObject task = MeasurementJsonConvertor.encodeToJson(entry.getValue().getDescription());
                        String taskstring = task.toString() + "\n";
                        writer.write(taskstring.getBytes());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
                writer.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 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 (currentSchedule) {
            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,
                                getApplicationContext());

                        // 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().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());
                        }

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

            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        updateSchedule(tasksToAdd, true);
    }

    @SuppressWarnings("unchecked")
    private void uploadResults() {
        MeasurementResult result;
        Future<MeasurementResult> future;
        JSONArray results = readResultsFromFile();

        synchronized (this.pendingTasks) {
            try {
                for (MeasurementTask task : this.pendingTasks.keySet()) {
                    future = this.pendingTasks.get(task);
                    if (future != null) {
                        sendStringMsg("Finished:\n" + task);
                        if (future.isDone()) {
                            try {
                                this.pendingTasks.remove(task);

                                if (!future.isCancelled()) {
                                    result = future.get();
                                } else {
                                    Logger.e("Task execution was canceled");
                                    JSONObject cancelledResult = MeasurementJsonConvertor.encodeToJson(this
                                            .getFailureResult(task, new CancellationException("Task cancelled")));
                                    results.put(cancelledResult);
                                }

                            } catch (InterruptedException e) {
                                Logger.e("Task execution interrupted", e);
                            } catch (ExecutionException e) {
                                if (e.getCause() instanceof MeasurementSkippedException) {
                                    // Don't do anything with this - no need to report skipped measurements
                                    sendStringMsg("Task skipped - " + e.getCause().toString() + "\n" + task);
                                    Logger.i("Task skipped", e.getCause());
                                } else {
                                    // Log the error
                                    sendStringMsg("Task failed - " + e.getCause().toString() + "\n" + task);
                                    Logger.e("Task execution failed", e.getCause());
                                    // Was already sent
                                    // finishedTasks.add(this.getFailureResult(task, e.getCause()));
                                }
                            } catch (CancellationException e) {
                                Logger.e("Task cancelled", e);
                            }
                        } else if (task.isPassedDeadline()) {
                            /*
                             * If a task has reached its deadline but has not been run, remove it and report
                             * failure
                             */
                            this.pendingTasks.remove(task);
                            future.cancel(true);
                            JSONObject cancelledResult = MeasurementJsonConvertor
                                    .encodeToJson(this.getFailureResult(task,
                                            new RuntimeException("Deadline passed before execution")));
                            results.put(cancelledResult);
                        }
                    }

                    if (future == null) {
                        /*
                         * Tasks that are scheduled after deadline are put into pendingTasks with a null future.
                         */
                        this.pendingTasks.remove(task);
                        JSONObject cancelledResult = MeasurementJsonConvertor.encodeToJson(
                                this.getFailureResult(task, new RuntimeException("Task scheduled after deadline")));
                        results.put(cancelledResult);
                    }
                }
            } 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");
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        if (results.length() > 0) {
            try {
                this.checkin.uploadMeasurementResult(results, resourceCapManager);
            } catch (IOException e) {
                Logger.e("Error when uploading message");
            }
        }

        Logger.i("A total of " + results.length() + " uploaded");
        Logger.i("A total of " + results.length() + " is in the results list");
    }

    private class CheckinTask implements Runnable {
        @Override
        public void run() {
            Logger.i("checking Speedometer service for new tasks");
            lastCheckinTime = Calendar.getInstance();
            try {
                persistState();
                uploadResults();
                getTasksFromServer();
                // Also reset checkin if we get a success
                resetCheckin();
                // Schedule the new tasks
                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 {
                PhoneUtils.getPhoneUtils().releaseWakeLock();
                updateStatus();
            }
        }
    }

    @SuppressWarnings("unused")
    private synchronized boolean isStopRequested() {
        return this.stopRequested;
    }

    private String getStackTrace(Throwable error) {
        final Writer result = new StringWriter();
        final PrintWriter printWriter = new PrintWriter(result);
        error.printStackTrace(printWriter);
        return result.toString();
    }

    private MeasurementResult getFailureResult(MeasurementTask task, Throwable error) {

        try {
            resourceCapManager.updateDataUsage(ResourceCapManager.PHONEUTILCOST);
        } catch (IOException e) {
            e.printStackTrace();
        }

        MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId,
                phoneUtils.getDeviceProperty(), task.getType(), System.currentTimeMillis() * 1000, false,
                task.measurementDesc);
        result.addResult("error", error.toString() + "\n" + getStackTrace(error));
        return result;
    }

    /**
     * A wrapper Callable class that broadcasts intents when the measurement starts and finishes.
     * Needed for activities to monitor the progress of user measurements.
     */
    private class UserMeasurementTask implements Callable<MeasurementResult> {
        MeasurementTask realTask;

        public UserMeasurementTask(MeasurementTask task) {
            realTask = task;
        }

        private void broadcastMeasurementStart() {
            Intent intent = new Intent();
            intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION);
            intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, MeasurementTask.USER_PRIORITY);
            MeasurementScheduler.this.sendBroadcast(intent);

            intent.setAction(UpdateIntent.SYSTEM_STATUS_UPDATE_ACTION);
            intent.putExtra(UpdateIntent.STATUS_MSG_PAYLOAD, realTask.getDescriptor() + " is running. ");

            MeasurementScheduler.this.sendBroadcast(intent);
        }

        private void broadcastMeasurementEnd(MeasurementResult result) {
            Intent intent = new Intent();
            intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION);
            intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, MeasurementTask.USER_PRIORITY);
            // A progress value greater than max progress to indicate the termination of a measurement
            intent.putExtra(UpdateIntent.PROGRESS_PAYLOAD, Config.MEASUREMENT_END_PROGRESS);

            if (result != null) {
                intent.putExtra(UpdateIntent.STRING_PAYLOAD, result.toString());
            } else {
                String errorString = "Measurement " + realTask.getDescriptor() + " has failed";
                errorString += "\nTimestamp: " + Calendar.getInstance().getTime();
                intent.putExtra(UpdateIntent.ERROR_STRING_PAYLOAD, errorString);
            }
            MeasurementScheduler.this.sendBroadcast(intent);
            // Update the status bar once the user measurement finishes
            updateStatus();
        }

        /**
         * The call() method that broadcast intents before the measurement starts and after the
         * measurement finishes.
         */
        @Override
        public MeasurementResult call() throws MeasurementError {
            MeasurementResult result = null;
            sendStringMsg("Running:\n" + realTask.toString());
            try {
                PhoneUtils.getPhoneUtils().acquireWakeLock();
                setCurrentTask(realTask);
                broadcastMeasurementStart();
                result = realTask.call();
            } finally {
                setCurrentTask(null);
                broadcastMeasurementEnd(result);
                PhoneUtils.getPhoneUtils().releaseWakeLock();
                sendStringMsg("Done running:\n" + realTask.toString());
                persistState();
            }
            return result;
        }
    }

    /**
     * Persist service state to prefs.
     */
    private synchronized void persistState() {
        Logger.d("Service persistState called");
        saveConsoleContent(systemResults, Config.PREF_KEY_SYSTEM_RESULTS);
        saveConsoleContent(userResults, Config.PREF_KEY_USER_RESULTS);
        saveConsoleContent(systemConsole, Config.PREF_KEY_SYSTEM_CONSOLE);
        saveStats();
    }

    /**
     * Restore service state from prefs.
     */
    private void restoreState() {
        Logger.d("Service restoreState called");
        initializeConsoles();
        restoreStats();
        loadSchedulerState();
    }

    /**
     * Save measurement statistics to persistent storage.
     */
    private void saveStats() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        SharedPreferences.Editor editor = prefs.edit();
        editor.putInt(Config.PREF_KEY_COMPLETED_MEASUREMENTS, completedMeasurementCnt);
        editor.putInt(Config.PREF_KEY_FAILED_MEASUREMENTS, failedMeasurementCnt);
        editor.commit();
    }

    /**
     * Restore measurement statistics from persistent storage.
     */
    private void restoreStats() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        completedMeasurementCnt = prefs.getInt(Config.PREF_KEY_COMPLETED_MEASUREMENTS, 0);
        failedMeasurementCnt = prefs.getInt(Config.PREF_KEY_FAILED_MEASUREMENTS, 0);
    }

    private boolean userConsented() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        boolean consented = prefs.getBoolean(Config.PREF_KEY_CONSENTED, false);
        Logger.i("userConsented returning " + consented);
        return consented;
    }

    /**
     * Persists the content of the console as a JSON string
     */
    private void saveConsoleContent(List<String> consoleContent, String prefKey) {
        Logger.d("Service saveConsoleContent for key " + prefKey);
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        SharedPreferences.Editor editor = prefs.edit();

        int length = consoleContent.size();
        Logger.d("Saving " + length + " entries to prefKey " + prefKey);
        ArrayList<String> items = new ArrayList<String>();
        // Since we use insertToConsole later on to restore the content, we have to store them
        // in the reverse order to maintain the same look
        for (int i = length - 1; i >= 0; i--) {
            items.add(consoleContent.get(i));
        }
        Type listType = new TypeToken<ArrayList<String>>() {
        }.getType();
        editor.putString(prefKey, MeasurementJsonConvertor.getGsonInstance().toJson(items, listType));
        editor.commit();
    }

    /**
     * Restores the console content from the saved JSON string
     */
    private void initializeConsoles() {
        Logger.d("Service initializeConsoles called");

        systemResults = new ArrayList<String>();
        restoreConsole(systemResults, Config.PREF_KEY_SYSTEM_RESULTS);
        if (systemResults.size() == 0) {
            insertStringToConsole(systemResults,
                    "Automatically-scheduled measurement results will " + "appear here.");
        }

        userResults = new ArrayList<String>();
        restoreConsole(userResults, Config.PREF_KEY_USER_RESULTS);
        if (userResults.size() == 0) {
            insertStringToConsole(userResults, "Your measurement results will appear here.");
        }

        systemConsole = new ArrayList<String>();
        restoreConsole(systemConsole, Config.PREF_KEY_SYSTEM_CONSOLE);
    }

    /**
     * Restores content for consoleContent with the key prefKey.
     */
    private void restoreConsole(List<String> consoleContent, String prefKey) {
        Logger.d("Service restoreConsole for " + prefKey);
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        String savedConsole = prefs.getString(prefKey, null);
        if (savedConsole != null) {
            Type listType = new TypeToken<ArrayList<String>>() {
            }.getType();
            ArrayList<String> items = MeasurementJsonConvertor.getGsonInstance().fromJson(savedConsole, listType);
            if (items != null) {
                Logger.d("Read " + items.size() + " items from prefkey " + prefKey);
                for (String item : items) {
                    insertStringToConsole(consoleContent, item);
                }
                Logger.d("Restored " + consoleContent.size() + " entries to console " + prefKey);
            }
        }
    }

    /**
     * Inserts a string into the console with the latest message on top.
     */
    private void insertStringToConsole(List<String> console, String msg) {
        if (msg != null) {
            console.add(0, msg);
            if (console.size() > Config.MAX_LIST_ITEMS) {
                console.remove(console.size() - 1);
            }
        }
    }

    /**
     * Adds a string to the corresponding console depending on whether the result is a user
     * measurement or a system measurement
     */
    private void updateResultsConsole(Intent intent) {
        int priority = intent.getIntExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, MeasurementTask.INVALID_PRIORITY);
        String msg = intent.getStringExtra(UpdateIntent.STRING_PAYLOAD);
        if (msg == null) {
            // Pull out error string instead
            msg = intent.getStringExtra(UpdateIntent.ERROR_STRING_PAYLOAD);
        }
        if (msg != null) {
            if (priority == MeasurementTask.USER_PRIORITY) {
                insertStringToConsole(userResults, msg);
            } else if (priority != MeasurementTask.INVALID_PRIORITY) {
                insertStringToConsole(systemResults, msg);
            }
        }
    }

    /**
     * Return a read-only list of the user results.
     */
    public synchronized List<String> getUserResults() {
        return Collections.unmodifiableList(userResults);
    }

    /**
     * Return a read-only list of the system results.
     */
    public synchronized List<String> getSystemResults() {
        return Collections.unmodifiableList(systemResults);
    }

    /**
     * Return a read-only list of the system console messages.
     */
    public synchronized List<String> getSystemConsole() {
        return Collections.unmodifiableList(systemConsole);
    }
}