/** Copyright 2015 Tim Engler, Rareventure LLC This file is part of Tiny Travel Tracker. Tiny Travel Tracker is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Tiny Travel Tracker is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>. */ package com.rareventure.gps2; import java.io.DataOutputStream; import java.io.IOException; import android.os.SystemClock; import android.util.Log; import com.rareventure.android.GpsReader; import com.rareventure.android.IntentTimer; import com.rareventure.android.TestUtil; import com.rareventure.android.WriteConstants; import com.rareventure.android.AndroidPreferenceSet.AndroidPreferences; /** * Decides when to turn on gps and when not to, and the locations to store, etc. * * TODO 3: if phone is turned on within a few seconds, regardless if it started moving or not, and * it was in a stopped condition, it is considered still stopped. * TODO 3: handle multiple readings when stopped. What do we do if the reading differ significantly? (should we send statistics so * we know if the general public is having problems?) * TODO 3: for exceptions in general, should we send stats? * TODO 3: store timeout stats so that we can determine how long gps takes? * TODO 3: failsafe... if thread is looping continously, tell us!!! * TODO 2.2: when phone is plugged, gps usage can be hoggy */ public class GpsTrailerGpsStrategy { /** * Set to the next time we must call update(). When we just start, we want to call it immediately */ private long nextSignificantEvent = 0; private GpsBatteryManager gpsBatteryManager = new GpsBatteryManager(); public static Preferences prefs = new Preferences(); private GpsReader gpsReader; private DataOutputStream os; public enum State { STOPPED, MOVING; } public State state = State.MOVING; /** * The start time of current (or last) stopped period. */ private long stoppedStartMs; private IntentTimer intentTimer; /** * @param gpsReader Turns gps reader on and off, but does not read directly from it */ public GpsTrailerGpsStrategy(DataOutputStream os, GpsReader gpsReader, IntentTimer intentTimer) { this.os = os; this.gpsReader = gpsReader; this.intentTimer = intentTimer; } private boolean isShutdownRequested; protected boolean isShutdown; private int successfulGpsAttempts; private int gpsAttempts; /** * Called when the gpsReader is ready to go */ public void start() { gpsBatteryManager.start(); } /** * Notify that the user has stopped moving based on the accelerometer readings * @param time time when first stopped (which may be in the past) */ public void stopped(long time) { boolean callUpdate = false; synchronized(this) { if(state == State.MOVING) { state = State.STOPPED; stoppedStartMs = time; callUpdate = true; } } //TODO 2.5: Incorporate stopped and moving into gps times? // if(callUpdate) // gpsBatteryManager.update(); } /** * Notify that the user has started moving */ public void moving() { boolean callUpdate = false; synchronized(this) { if(state == State.STOPPED) { state = State.MOVING; callUpdate = true; } } // if(callUpdate) // update(); } public void gotReading() { synchronized(this) { gpsBatteryManager.lastGpsReadingFromBootMs = SystemClock.elapsedRealtime(); gpsBatteryManager.lastReadingSuccessful = true; this.notify(); successfulGpsAttempts++; intentTimer.writeDebug("Got reading"); } } private void writeUpdateStatus() { synchronized(TestUtil.class) { try { long timeSincePhoneBoot = SystemClock.elapsedRealtime(); TestUtil.writeMode(os, WriteConstants.MODE_WRITE_STRATEGY_STATUS); TestUtil.writeBoolean("gpsOn", os, this.gpsBatteryManager.gpsOn); TestUtil.writeTime("gpsAttemptStartedFromPhoneBootMs", os, this.gpsBatteryManager.gpsAttemptStartedFromPhoneBootMs); TestUtil.writeTime("gpsAttemptEndedFromPhoneBootMs", os, this.gpsBatteryManager.gpsAttemptEndedFromPhoneBootMs); TestUtil.writeTime("lastGpsReadingFromBootMs", os, this.gpsBatteryManager.lastGpsReadingFromBootMs); TestUtil.writeTime("lastGpsStatsUpdateFromPhoneBootMs", os, this.gpsBatteryManager.lastGpsStatsUpdateFromPhoneBootMs); TestUtil.writeLong("nextSignificantEventTime", os, this.nextSignificantEvent - timeSincePhoneBoot); TestUtil.writeLong("freeGpsTimeMs", os, this.gpsBatteryManager.calcFreeGpsTimeMs(timeSincePhoneBoot)); TestUtil.writeLong("totalTimeGpsRunningMs", os, this.gpsBatteryManager.totalTimeGpsRunningMs); TestUtil.writeLong("totalTimeNotRunningGpsMs", os, timeSincePhoneBoot - this.gpsBatteryManager.startTimeFromPhoneBootMs - this.gpsBatteryManager.totalTimeGpsRunningMs); TestUtil.writeLong("totalSuccessfulGpsTries", os, successfulGpsAttempts); TestUtil.writeLong("totalGpsTries", os, gpsAttempts); TestUtil.writeLong("longTimeWanted", os, this.desireManager.longTimeWanted); TestUtil.writeLong("shortTimeWanted", os, this.desireManager.shortTimeWanted); intentTimer.writeDebug( "gps use perc is "+prefs.batteryGpsOnTimePercentage ); } catch (IOException e) { e.printStackTrace(); // throw new IllegalStateException(e); } } } public class GpsBatteryManager { public boolean lastReadingSuccessful; /** * The time we started the program in general. NOTE: this number is fudged a little * to faciliate starting gps right away */ private long startTimeFromPhoneBootMs; /** * Last time we updated the gps stats */ private long lastGpsStatsUpdateFromPhoneBootMs; /** * The start of the last gps session (see also gpsOn) */ private long gpsAttemptStartedFromPhoneBootMs; /** * The end of the last gps session (see also gpsOn) */ private long gpsAttemptEndedFromPhoneBootMs; /** * The last time a successful gps reading was taken. */ private long lastGpsReadingFromBootMs; /** * Total time gps has been running since we started */ private long totalTimeGpsRunningMs; private boolean gpsOn; /** * This looks at the current stats and determines whether to turn gps on or * leave it off. * * This method must be called from the strategy thread only (to prevent * deadlocks). If you want to update, synchronize notify the strategy thread * @param deltaTimeMs this is the delta time we actually waited. Since android * can suddenly decide to ignore everything and go to sleep * we can't use the current time as an indication of how * long the gps has been turned on. * * @return whether to turn gps on or not (which must be turned on or off in * a non synchronized block */ protected boolean updateFromStrategyThreadOnly(long deltaTimeMs) { intentTimer.writeDebug("deltaTimeMs "+deltaTimeMs); long timeFromPhoneBootMs = SystemClock.elapsedRealtime(); //first lets update the stats so far if(gpsOn) { totalTimeGpsRunningMs += deltaTimeMs; } //total time available for running gps, subtracting time already spent long gpsTimeAvailable = calcFreeGpsTimeMs(timeFromPhoneBootMs); //we don't want to allow too much gps time. This is to prevent wasting the battery needlessly if we // had a stroke of luck and we're able to get the gps time very easily //Were basically truncating the amount of time we have allocated to use gps if(gpsTimeAvailable > prefs.maxGpsTimeMs) { //fudge the stats so we have at most prefs.maxGpsTimeMs of time left to allocate //TODO 3 maybe we shouldn't be fudging this value. // gpsTimeAvailable = (long) ((timeFromPhoneBootMs - startTimeFromPhoneBootMs) * prefs.batteryGpsOnTimePercentage - // totalTimeGpsRunningMs); totalTimeGpsRunningMs = (long)((timeFromPhoneBootMs - startTimeFromPhoneBootMs) * prefs.batteryGpsOnTimePercentage) - prefs.maxGpsTimeMs + 1; gpsTimeAvailable = prefs.maxGpsTimeMs; } if(gpsOn) { long currentTimeGpsRunning = timeFromPhoneBootMs - gpsAttemptStartedFromPhoneBootMs; //if the desiremanager is satisfied, we successfully read a gps point, or //the absolute time left to perform a gps reading is used up, // we turn the gps off if(currentTimeGpsRunning >= desireManager.currTimeWanted || gpsTimeAvailable <= 0 || lastReadingSuccessful) { gpsAttemptEndedFromPhoneBootMs = timeFromPhoneBootMs; if(lastReadingSuccessful) { desireManager.updateDesiresForSuccessfulReading(gpsAttemptEndedFromPhoneBootMs - gpsAttemptStartedFromPhoneBootMs); lastReadingSuccessful = false; } else desireManager.updateDesiresForUnsuccessfulReading(gpsTimeAvailable); nextSignificantEvent = desireManager.waitTimeMs + timeFromPhoneBootMs; return false; } else //we want to keep gps on { nextSignificantEvent = desireManager.currTimeWanted - currentTimeGpsRunning + timeFromPhoneBootMs; return true; } } else // gps is off { long currentTimeWaiting = timeFromPhoneBootMs - gpsAttemptEndedFromPhoneBootMs; if(currentTimeWaiting >= desireManager.waitTimeMs) { //we want to turn it on gpsAttemptStartedFromPhoneBootMs = timeFromPhoneBootMs; long timeToLeaveGpsOn = desireManager.currTimeWanted; if(desireManager.currTimeWanted > calcFreeGpsTimeMs(timeFromPhoneBootMs+desireManager.currTimeWanted)) { //TODO 3 this is a hack to print a warning out to the wake lock debug file // I should probably have just a general file for extended log messages intentTimer.writeDebug("gps desire manager has asked for more than allowed time," +" gpsTimeAvailable: "+gpsTimeAvailable +", desireManager.currTimeWanted: "+desireManager.currTimeWanted); timeToLeaveGpsOn = gpsTimeAvailable; } nextSignificantEvent = timeToLeaveGpsOn + timeFromPhoneBootMs; gpsAttempts++; return true; } else { nextSignificantEvent = desireManager.waitTimeMs + gpsAttemptEndedFromPhoneBootMs; return false; } } } //end GpsBatteryMeter.update() /** * Returns total time that is allowed to be allocated to gps, given the time we * have already spent * @param currTimeMs * @return */ public long calcFreeGpsTimeMs(long currTimeMs) { intentTimer.writeDebug("currTimeMs "+currTimeMs+" startTimeFromPhoneBoot "+startTimeFromPhoneBootMs +"totalTimeGpsRunning "+totalTimeGpsRunningMs); return (long) ((currTimeMs - startTimeFromPhoneBootMs) * prefs.batteryGpsOnTimePercentage - totalTimeGpsRunningMs); } public void start() { desireManager.updateDesiresForStart(); //we start with a 5 minute leeway so the code can turn on gps right away, rather than waiting gpsAttemptEndedFromPhoneBootMs = startTimeFromPhoneBootMs = SystemClock.elapsedRealtime() - prefs.extraTimeForStartMs; strategyThread.start(); } //PERF: consider consolidating thread into a looper or a HandlerTimer or something private Thread strategyThread = new Thread() { public void run() { intentTimer.acquireWakeLock(); try { boolean wantGps = true; while(!isShutdownRequested) { /* ttt_installer:remove_line */intentTimer.writeDebug("About to enter synchronization block"); //this thread is responsible for turning on and off gpsreader //as well as updating nextsignificantevent and calling the desireManager //to determine how long to wait and how much time to use for each gps reading synchronized(GpsTrailerGpsStrategy.this) { //we used elapsed real time, because using System.currentTimeMillis() is unreliable: //From http://developer.android.com/reference/android/os/SystemClock.html //The wall clock can be set by the user or the phone network (see setCurrentTimeMillis(long)), // so the time may jump backwards or forwards unpredictably. This clock should only be used when // correspondence with real-world dates and times is important, such as in a calendar or alarm // clock application. Interval or elapsed time measurements should use a different clock. If you // are using System.currentTimeMillis(), consider listening to the ACTION_TIME_TICK, // ACTION_TIME_CHANGED and ACTION_TIMEZONE_CHANGED Intent broadcasts to find out when the time changes. long timeFromPhoneBootMs = SystemClock.elapsedRealtime(); long waitTimeMs = 0; if(nextSignificantEvent > timeFromPhoneBootMs) { /* ttt_installer:remove_line */intentTimer.writeDebug("About to wait for "+(nextSignificantEvent - timeFromPhoneBootMs)); //we only turn off our wake lock if we aren't running gps //note, not sure if this is necessary, but even if it works for my //phone, it may not work for other phones, so to be sure, we //keep the cpu on //co: tim trying this off to see what happen if(!wantGps) { intentTimer.sleepUntil(nextSignificantEvent); /* ttt_installer:remove_line */intentTimer.writeDebug("Turned off wake lock"); } if(isShutdownRequested) return; timeFromPhoneBootMs = SystemClock.elapsedRealtime(); waitTimeMs = nextSignificantEvent - timeFromPhoneBootMs; GpsTrailerGpsStrategy.this.wait( waitTimeMs ); intentTimer.acquireWakeLock(); /* ttt_installer:remove_line */intentTimer.writeDebug("Acquired wake lock, done waiting for "+(nextSignificantEvent - timeFromPhoneBootMs)); timeFromPhoneBootMs = SystemClock.elapsedRealtime(); } if(isShutdownRequested) break; /* ttt_installer:remove_line */intentTimer.writeDebug("About to update strategy"); wantGps = updateFromStrategyThreadOnly(waitTimeMs); lastGpsStatsUpdateFromPhoneBootMs = timeFromPhoneBootMs; } /* ttt_installer:remove_line */intentTimer.writeDebug("About to update gpsReader to "+wantGps); //we don't want deadlocks, so we keep this outside of the synchronized loop if(wantGps) gpsReader.turnOn(); else gpsReader.turnOff(); /* ttt_installer:remove_line */intentTimer.writeDebug("About to set gpsOn and writeUpdateStatus"); synchronized(GpsTrailerGpsStrategy.this) { gpsOn = wantGps; writeUpdateStatus(); } /* ttt_installer:remove_line */intentTimer.writeDebug("Looping while not shutdown"); } //while running /* ttt_installer:remove_line */intentTimer.writeDebug("Gps Strategy is shutdown"); } catch (Exception e) { e.printStackTrace(); try { TestUtil.writeException(os ,e); } catch (IOException e1) { e1.printStackTrace(); } } finally { intentTimer.releaseWakeLock(); } synchronized(GpsTrailerGpsStrategy.this) { GpsTrailerGpsStrategy.this.isShutdown = true; GpsTrailerGpsStrategy.this.notify(); } } //end run // private void writeGpsStrategyThreadStatus(long timeToSleep, byte mode) { // if(os == null) // return; // // synchronized(TestUtil.class) // { // try { // TestUtil.writeMode(os, WriteConstants.MODE_WRITE_STRATEGY_STATUS); // TestUtil.writeTime("currTime", os, System.currentTimeMillis()); // TestUtil.writeLong("timeToSleep", os, timeToSleep); // TestUtil.writeByte("mode, 2=update, 1=sleep", os, mode); // } catch (IOException e) { // // e.printStackTrace(); // throw new IllegalStateException(e); // } // } // // } // }; } // end class GpsBatteryManager public static class Preferences implements AndroidPreferences { /** * The amount of extra time to allow the gps reading to go if * we really want to have the data */ public int gpsBoostMs; //TODO 3: fix this /** * The maximum time we are allowed to have to allocate. * We will truncate to this time. * <p> * If we had a stroke of luck and were able to take many readings * with a small amount of time, we don't want to waste the rest * if we obviously can't take a measurement. * * This is not superseced by gpsTimeWantedMaxMs because if we * are having a stroke of luck, this will bleed off excess gps time * that we otherwise use when gps readings are not so easy to get. * * This should not be less than maxLongTimeWantedMs */ public long maxGpsTimeMs = 30 * 60 * 1000; /** * Max time the gps should be as a percentage of total time */ public float batteryGpsOnTimePercentage = .10f; /** * Minimum time that the accelerometer is stopped before we consider the user to * have actually rested */ public long minStopTimeForGpsReadingMs = 60*1000; /** * Amount of time we want initially for short period readings */ public long initialShortTimeWantedMs = 15*1000 ; public long initialLongTimeWanted = 2 * 60 * 1000; /** * This describes the amount of extra time to save when we take a short gps readings. * Basically, we have a bank of gps time, and when we run a short gps reading, we subtract * from that bank. * <p>So, everytime we run a short gps reading, we set the wait time longer than would be * required to save so we can do a long reading</p> * <p>This multiplied by the current short time gps is the time we target to save * when we lengthen the wait time.</p> */ public float extraWaitTimeShortTimeMultiplier = 1f; /** * Multiplier to reduce short time after a successful gps reading */ public float shortTimeSuccessfulMultiplier = .9f; /** * Multiplier to lengthen short time after an unsuccessful gps reading */ public float shortTimeUnsuccessfulMultipler = 1.1f; /** * Minimum short time wanted */ public long shortTimeMinMs = 5*1000; /** * Minimum short time wanted compared to last successful reading. * If the last gps was successful and the short time falls below * the amount of time it took to read the last gps multiplied by * this percentage, we set it to this minimum. */ public float minReadingTimeMultipler = 1.3f; /** * The minimum time a long gps measurement can be as a multiplier * of the last successful measurement. The long * gps measurement will be reset here when a successful reading * was taken */ public float minLongTimeOfShortTimeMultiplier = 3f; /** * The amount to multiply longTimeWanted when a long run was unsuccessful */ public float longTimeMultiplier = 2f; /** * Maximum time a long gps try can be. This should not be greater than maxGpsTimeMs */ public long maxLongTimeWantedMs = 20 * 1000 * 60; /** * Amount of time to increase the short time multiplier on an * unsuccessful measurement */ public float shortTimeUnsuccessfulMultiplier = 1.1f; /** * The maximum time a short run can be */ public long shortTimeMaxMs = 30 * 1000; /** * Extra time for when we start so we turn on gps right away */ public long extraTimeForStartMs = (long) (initialShortTimeWantedMs * (this.extraWaitTimeShortTimeMultiplier + 1) / batteryGpsOnTimePercentage) + 1; } public void shutdown() { synchronized(this) { isShutdownRequested = true; notify(); while(!isShutdown) try { wait(); } catch (InterruptedException e) { throw new IllegalStateException(e); } } } private DesireManager desireManager = new DesireManager(); private class DesireManager { private long shortTimeWanted; private long longTimeWanted; /** * The current amount of time wanted */ private long currTimeWanted; /** * time to wait until next reading (from previous reading) */ private long waitTimeMs; private void updateDesiresForStart() { shortTimeWanted = prefs.initialShortTimeWantedMs; longTimeWanted = prefs.initialLongTimeWanted; currTimeWanted = shortTimeWanted; waitTimeMs = 0; } /** * Updates what this strategy wants to do next given * that the last gps reading was successful * * @param readingTimeMs it took to read from the GPS */ private void updateDesiresForSuccessfulReading(long readingTimeMs) { //if the reading time is less than the current short time if(readingTimeMs < shortTimeWanted) { //we divide out shortTimeUnsuccessfulMultipler since we always update //short time wanted as if it failed whenever we start shortTimeWanted = (long) (shortTimeWanted * prefs.shortTimeSuccessfulMultiplier / prefs.shortTimeUnsuccessfulMultipler)+1 ; if(shortTimeWanted < prefs.shortTimeMinMs) shortTimeWanted = prefs.shortTimeMinMs; //if it took x seconds to read the gps last time, we don't want to reduce //the gps reader to take less than x seconds this time. if(shortTimeWanted < readingTimeMs * prefs.minReadingTimeMultipler) shortTimeWanted = (long) (readingTimeMs * prefs.minReadingTimeMultipler)+1; } //reset long time wanted back to the minimum whenever we get a reading longTimeWanted = (long) (prefs.minLongTimeOfShortTimeMultiplier * shortTimeWanted) + 1; waitTimeMs = calculateAbsTimeNeeded((long) (shortTimeWanted * prefs.extraWaitTimeShortTimeMultiplier + shortTimeWanted)+1); currTimeWanted = shortTimeWanted; } /** * Updates what this strategy wants to do next given * that the last gps reading was unsuccessful * * @param spareReadingTime the amount of time we are allowed to read from the gps. * Ex. if the user has the gps on set to 10%, TTT has been * running for 100 minutes, we've turned on GPS for 5 minutes * already, then we have 100 * .10 - 5 = 5 minutes allowed to * read from the gps. */ private void updateDesiresForUnsuccessfulReading(long spareReadingTime) { //we need to budget our time for gps so we can run short runs periodically, and //if enough time has passed, then do a long run long totalWantedGpsTimeMs = (long) (shortTimeWanted * prefs.extraWaitTimeShortTimeMultiplier + shortTimeWanted)+1; waitTimeMs = calculateAbsTimeNeeded(totalWantedGpsTimeMs); //if there is enough time to do a long run if(totalWantedGpsTimeMs + spareReadingTime >= longTimeWanted) { currTimeWanted = longTimeWanted; //update long time wanted as if the current run will fail longTimeWanted = (long) (longTimeWanted * prefs.longTimeMultiplier) + 1; if(longTimeWanted > prefs.maxLongTimeWantedMs ) longTimeWanted = prefs.maxLongTimeWantedMs; } else //do a short run { currTimeWanted = shortTimeWanted; //update short time wanted as if the current run will fail shortTimeWanted *= prefs.shortTimeUnsuccessfulMultiplier; if(shortTimeWanted > prefs.shortTimeMaxMs) shortTimeWanted = prefs.shortTimeMaxMs; } } /** * Simply calculates the total time needed to wait for a * certain amount of gps time, ignoring any current data. * i.e. regardless of the situation, this will always return * the same value * * @param timeWanted * @return */ private long calculateAbsTimeNeeded(long timeWanted) { return (long) (timeWanted / prefs.batteryGpsOnTimePercentage - timeWanted + 1); } } public void notifyWoken() { //make sure we stay awake until we want to sleep again // intentTimer.writeDebug("gps manager notify woken"); intentTimer.acquireWakeLock(); synchronized(this) { this.notify(); } } }