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