/* 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 com.mobiperf.measurements.DnsLookupTask; import com.mobiperf.measurements.HttpTask; import com.mobiperf.measurements.PingTask; import com.mobiperf.measurements.RRCTask; import com.mobiperf.measurements.TCPThroughputTask; import com.mobiperf.measurements.TracerouteTask; import com.mobiperf.measurements.UDPBurstTask; import com.mobiperf.util.MeasurementJsonConvertor; import com.mobiperf.util.PhoneUtils; import android.content.Context; import android.content.Intent; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.Calendar; import java.util.concurrent.Callable; import org.json.JSONException; /** * A basic power manager implementation that decides whether a measurement can be scheduled * based on the current battery level: no measurements will be scheduled if the current battery * is lower than a threshold. */ public class ResourceCapManager { public enum DataUsageProfile { PROFILE1, PROFILE2, PROFILE3, PROFILE4, UNLIMITED } /** The minimum threshold below which no measurements will be scheduled */ private int minBatteryThreshold; private Context context = null; private int dataLimit;//in Byte private DataUsageProfile dataUsageProfile; // Constants for how much data can be consumed under each profile private static int UNLIMITED_LIMIT = -1; private static int PROFILE1_LIMIT = 50 * 1024 * 1024; private static int PROFILE2_LIMIT = 100 * 1024 * 1024; private static int PROFILE3_LIMIT = 250 * 1024 * 1024; private static int PROFILE4_LIMIT = 500 * 1024 * 1024; // Looking up various phone util data uses the network. // It's hard to measure how much. // The good news is that this value is basically constant! public static int PHONEUTILCOST = 3 * 1024; public ResourceCapManager(int batteryThresh, Context context) { this.minBatteryThreshold = batteryThresh; this.dataLimit=PROFILE3_LIMIT; this.context=context; this.dataUsageProfile=DataUsageProfile.PROFILE3; } /** * Sets the minimum battery percentage below which measurements cannot be run. * * @param batteryThresh the battery percentage threshold between 0 and 100 */ public synchronized void setBatteryThresh(int batteryThresh) throws IllegalArgumentException { if (batteryThresh < 0 || batteryThresh > 100) { throw new IllegalArgumentException("batteryCap must fall between 0 and 100, inclusive"); } this.minBatteryThreshold = batteryThresh; } public synchronized int getBatteryThresh() { return this.minBatteryThreshold; } /** * Given a data profile string, set the data limit and profile code accordingly. * * If an invalid code is given, leave as default (250 MB) and print a warning. * * @param dataLimitStr String describing the profile */ public synchronized void setDataUsageLimit(String dataLimitStr) { if (dataLimitStr.equals("50 MB")) { dataLimit = PROFILE1_LIMIT; dataUsageProfile = DataUsageProfile.PROFILE1; } else if (dataLimitStr.equals("100 MB")) { dataLimit = PROFILE2_LIMIT; dataUsageProfile = DataUsageProfile.PROFILE2; } else if (dataLimitStr.equals("250 MB")) { dataLimit = PROFILE3_LIMIT; dataUsageProfile = DataUsageProfile.PROFILE3; } else if (dataLimitStr.equals("500 MB")) { dataLimit = PROFILE4_LIMIT; dataUsageProfile = DataUsageProfile.PROFILE4; } else if (dataLimitStr.equals("Unlimited")) { dataLimit = UNLIMITED_LIMIT; dataUsageProfile = DataUsageProfile.UNLIMITED; } else { Logger.w("Specified limit " + dataLimitStr + " not found!"); } } /** * @return The current data limit in bytes. */ public synchronized int getDataLimit() { return this.dataLimit; } /** * @return An enum representing the data usage limit. */ public synchronized DataUsageProfile getDataUsageProfile(){ return this.dataUsageProfile; } /** * Reset the data used in the data usage file to 0. * This should never be done unless the file does not exist. */ private void resetDataUsage() { File file = new File(context.getFilesDir(), "datausage"); if (file.exists()) { Logger.e("Attempting to overwrite a file that exists!!!!"); } long usageStartTimeSec = (System.currentTimeMillis() / 1000); writeDataUsageToFile(0, usageStartTimeSec); } /** * Store the data used this period and the beginning of the period in a file, * in the format [time reset, in seconds]_[bytes used]. * * Note that the data used can be negative, due to a underused data budget * from last period. * * @param dataUsed The updated amount of data to write * @param time The updated time to write */ private synchronized void writeDataUsageToFile(long dataUsed, long time) { try { FileOutputStream outputStream = context.openFileOutput("datausage", Context.MODE_PRIVATE); String usageStat = time + "_" + dataUsed; outputStream.write(usageStat.getBytes()); Logger.i("Updating data usage: " + dataUsed + " Byte used from " + time); outputStream.close(); } catch (IOException e) { Logger.e("Error in creating data usage file"); e.printStackTrace(); } } /** * Read the usage data (start of usage period and quantity used in bytes) * from the usage data file. * * @return An array consisting of the start of the usage period, then the data * used so far. If the file does not exist, returns -1 in each argument. */ private synchronized long[] readUsageFromFile() { long[] retval = {-1, -1}; File file = new File(context.getFilesDir(), "datausage"); if (!file.exists()) { return retval; } try { String content = ""; BufferedReader br = new BufferedReader(new FileReader(file)); String line; while ((line = br.readLine()) != null) { content += line; } String[] toks = content.split("_"); long usageStartTimeSec = Long.parseLong(toks[0]); long dataUsed = Long.parseLong(toks[1]); retval[0] = usageStartTimeSec; retval[1] = dataUsed; br.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return retval; } /** * Updates the data consumption period: using the current time move ahead to the * correct data consumption period, and also update the data used so far. * * Data assigned to a previous data period is subtracted; this can go below zero, * effectively crediting unused data to future tasks. * * @param dataUsed Data consumed since the start of the last period * @param usageStartTimeSec Time since the start of the last period * @return */ private long setNewDataConsumptionPeriod(long dataUsed, long usageStartTimeSec) { long time_per_period = Config.DEFAULT_DATA_MONITOR_PERIOD_DAY * 24 * 60 * 60; Logger.i("Finished data consumption period that began at time:" + usageStartTimeSec + " having " +dataUsed + " consumed"); // Figure out how many periods have passed int periods = (int) (((float) ((long) (System.currentTimeMillis() / 1000) - usageStartTimeSec)) / (float) time_per_period); // Update usageStarTimeSec to the appropriate period usageStartTimeSec += periods * time_per_period; // Discount from the data used data that is budgeted to previous periods. // Note that this could go less than zero if we are below budget. long datalimit_per_period = (getDataLimit() * Config.DEFAULT_DATA_MONITOR_PERIOD_DAY) / 30; dataUsed = dataUsed - (((int) (periods)) * datalimit_per_period); Logger.i("Net data usage at start of period: " + dataUsed); writeDataUsageToFile(dataUsed, usageStartTimeSec); return dataUsed; } /** * Helper function: given the beginning of the data usage period currrently * under consideration, determine if we're still in that period. * * @param usageStartTimeSec The start of the last stored data usage period * @return True if we are still in the same data usage period. */ private boolean isInDataLimitPeriod(long usageStartTimeSec) { long timeSoFar = (System.currentTimeMillis() / 1000) - usageStartTimeSec; Logger.i("Time passed since data period last changed: " + timeSoFar); return timeSoFar <= Config.DEFAULT_DATA_MONITOR_PERIOD_DAY * 24 * 60 * 60; } /** * Determines if the data limit has been exceeded. * * If there is no data limit, always returns false. * If there is no valid data usage file, * creates a new one and returns false. * * Otherwise, checks if we are over the limit yet or if we can run another task. * If a new data period needs to be started, we do that too. * * * @param nextTaskType In the case of a TCP throughput task, we only run it if there is * enough data left. * @return True if over the data limit * @throws IOException */ private boolean isOverDataLimit(String nextTaskType) throws IOException { Logger.i("Checking data limit..."); if (getDataLimit() == UNLIMITED_LIMIT) { Logger.i("No data limit!"); return false; } long[] usagedata = readUsageFromFile(); long usageStartTimeSec = usagedata[0]; long dataUsed = usagedata[1]; if (usageStartTimeSec != -1) { if (!isInDataLimitPeriod(usageStartTimeSec)) { // Update our file to the next period, and update our data usage // budget accordingly. dataUsed = setNewDataConsumptionPeriod(dataUsed, usageStartTimeSec); } long dataLimit = (getDataLimit() * Config.DEFAULT_DATA_MONITOR_PERIOD_DAY) / 30; Logger.i("Data limit is: " + dataLimit + " Data used is:" + dataUsed); if (dataUsed >= dataLimit) { Logger.i("Exceeded data limit: Total data limit:" + getDataLimit()); return true; } else { return false; } } // If the file wasn't there we need to reset the data limit period. resetDataUsage(); return false; } /** * Determine how much data was consumed by a task and update the * data usage accordingly. * * @param result Structure holding the measurement result from which we can extract data usage. * @param taskType The type of measurement task completed * @throws IOException */ public void updateDataUsage(long taskDataUsed) throws IOException { Logger.i("Amount of data used in the last task: " + taskDataUsed); long[] usagedata = readUsageFromFile(); long usageStartTimeSec = usagedata[0]; long dataUsed = usagedata[1]; // If we have a valid file if (usageStartTimeSec != -1) { dataUsed += taskDataUsed; if (! isInDataLimitPeriod(usageStartTimeSec)) { // If we are in a new data consumption period, update it setNewDataConsumptionPeriod(dataUsed, usageStartTimeSec); } else { // Otherwise just write to a file writeDataUsageToFile(dataUsed, usageStartTimeSec); } } else { // If we don't have a data usage file, initialize it with the data just used Logger.i("Data usage file not found, creating a new one..."); usageStartTimeSec = (System.currentTimeMillis() / 1000); dataUsed = taskDataUsed; writeDataUsageToFile(dataUsed, usageStartTimeSec); } } /** * Returns whether a measurement can be run. */ public synchronized boolean canScheduleExperiment() { return (PhoneUtils.getPhoneUtils().isCharging() || PhoneUtils.getPhoneUtils().getCurrentBatteryLevel() > minBatteryThreshold); } /** * A task wrapper that is power aware, the real logic is carried out by realTask * * @author wenjiezeng@google.com (Steve Zeng) * */ public static class PowerAwareTask implements Callable<MeasurementResult> { private MeasurementTask realTask; private ResourceCapManager pManager; private MeasurementScheduler scheduler; public PowerAwareTask(MeasurementTask task, ResourceCapManager manager, MeasurementScheduler scheduler) { realTask = task; pManager = manager; this.scheduler = scheduler; } private void broadcastMeasurementStart() { Logger.i("Starting PowerAwareTask " + realTask); Intent intent = new Intent(); intent.setAction(UpdateIntent.SYSTEM_STATUS_UPDATE_ACTION); intent.putExtra(UpdateIntent.STATUS_MSG_PAYLOAD, "Running " + realTask.getDescriptor()); scheduler.sendBroadcast(intent); } private void broadcastMeasurementEnd(MeasurementResult result, MeasurementError error) { Logger.i("Ending PowerAwareTask " + realTask); // Only broadcast information about measurements if they are true errors. if (!(error instanceof MeasurementSkippedException)) { Intent intent = new Intent(); intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, (int) realTask.getDescription().priority); // A progress value MEASUREMENT_END_PROGRESS indicates the end of an 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.toString() + " failed. "; errorString += "\n\nTimestamp: " + Calendar.getInstance().getTime(); if (error != null) { errorString += "\n\n" + error.toString(); } intent.putExtra(UpdateIntent.ERROR_STRING_PAYLOAD, errorString); } // We now store results as strings to disk immediately to avoid data // losses on a crash, so convert to a JSON and sent back try { intent.putExtra(UpdateIntent.RESULT_PAYLOAD, MeasurementJsonConvertor.encodeToJson(result).toString()); } catch (JSONException e) { e.printStackTrace(); } scheduler.sendBroadcast(intent); } scheduler.updateStatus(); } @Override public MeasurementResult call() throws MeasurementError { MeasurementResult result = null; scheduler.sendStringMsg("Running:\n" + realTask.toString()); try { PhoneUtils.getPhoneUtils().acquireWakeLock(); if (scheduler.isPauseRequested()) { Logger.i("Skipping measurement - scheduler paused"); throw new MeasurementSkippedException("Scheduler paused"); } if (!pManager.canScheduleExperiment()) { Logger.i("Skipping measurement - low battery"); throw new MeasurementSkippedException("Not enough battery power"); } if (PhoneUtils.getPhoneUtils().getCurrentNetworkConnection()==PhoneUtils.TYPE_MOBILE){ try { if(pManager.isOverDataLimit(realTask.getMeasurementType())) { scheduler.sendStringMsg("No cellular data is available for a server " + realTask.getDescription().type+" task"); Logger.i("Skipping measurement - data limit is passed"); throw new MeasurementSkippedException("Over data limit"); } } catch (IOException e) { Logger.e("Exception occured during R/Wing of data stat file"); e.printStackTrace(); } } scheduler.setCurrentTask(realTask); broadcastMeasurementStart(); try { Logger.i("Calling PowerAwareTask " + realTask); pManager.updateDataUsage(PHONEUTILCOST); result = realTask.call(); Logger.i("Got result " + result); // We only care about the data usage when on the mobile network if (PhoneUtils.getPhoneUtils().getCurrentNetworkConnection()==PhoneUtils.TYPE_MOBILE){ pManager.updateDataUsage(realTask.getDataConsumed()); } broadcastMeasurementEnd(result, null); return result; } catch (MeasurementError e) { Logger.e("Got MeasurementError running task", e); broadcastMeasurementEnd(null, e); throw e; } catch (Exception e) { Logger.e("Got exception running task", e); MeasurementError err = new MeasurementError("Got exception running task", e); broadcastMeasurementEnd(null, err); throw err; } } finally { PhoneUtils.getPhoneUtils().releaseWakeLock(); scheduler.setCurrentTask(null); scheduler.sendStringMsg("Done running:\n" + realTask.toString()); } } } }