/* * 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.mobilyzer; import com.mobilyzer.MeasurementScheduler.DataUsageProfile; import com.mobilyzer.util.Logger; import com.mobilyzer.util.PhoneUtils; import android.content.Context; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; /** * 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 { /** 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, 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(DataUsageProfile profile) { // 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!"); // } dataUsageProfile=profile; if (profile.equals(DataUsageProfile.PROFILE1)) { dataLimit = PROFILE1_LIMIT; } else if (profile.equals(DataUsageProfile.PROFILE2)) { dataLimit = PROFILE2_LIMIT; } else if (profile.equals(DataUsageProfile.PROFILE3)) { dataLimit = PROFILE3_LIMIT; } else if (profile.equals(DataUsageProfile.PROFILE4)) { dataLimit = PROFILE4_LIMIT; } else if (profile.equals(DataUsageProfile.UNLIMITED)) { dataLimit = UNLIMITED_LIMIT; } else { Logger.w("Specified limit profile 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 */ public 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); } }