/*
Copyright (C) 2011 The University of Michigan
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
Please send inquiries to powertutor@umich.edu
*/
package edu.umich.PowerTutor.components;
import edu.umich.PowerTutor.PowerNotifications;
import edu.umich.PowerTutor.phone.PhoneConstants;
import edu.umich.PowerTutor.service.IterationData;
import edu.umich.PowerTutor.service.PowerData;
import edu.umich.PowerTutor.util.NotificationService;
import edu.umich.PowerTutor.util.Recycler;
import edu.umich.PowerTutor.util.SystemInfo;
import android.content.Context;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.LocationManager;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;
public class GPS extends PowerComponent {
public static class GpsData extends PowerData {
private static Recycler<GpsData> recycler = new Recycler<GpsData>();
public static GpsData obtain() {
GpsData result = recycler.obtain();
if(result != null) return result;
return new GpsData();
}
/* The time in seconds since the last iteration of data. */
public double[] stateTimes;
/* The number of satellites. This number is only available while the GPS is
* in the on state. Otherwise it is 0.
*/
public int satellites;
private GpsData() {
stateTimes = new double[GPS.POWER_STATES];
}
public void init(double[] stateTimes, int satellites) {
for(int i = 0; i < GPS.POWER_STATES; i++) {
this.stateTimes[i] = stateTimes[i];
}
this.satellites = satellites;
}
@Override
public void recycle() {
recycler.recycle(this);
}
@Override
public void writeLogDataInfo(OutputStreamWriter out) throws IOException {
StringBuilder res = new StringBuilder();
res.append("GPS-state-times");
for(int i = 0; i < GPS.POWER_STATES; i++) {
res.append(" ").append(stateTimes[i]);
}
res.append("\nGPS-sattelites ").append(satellites).append("\n");
out.write(res.toString());
}
}
public static final int POWER_STATES = 3;
public static final int POWER_STATE_OFF = 0;
public static final int POWER_STATE_SLEEP = 1;
public static final int POWER_STATE_ON = 2;
public static final String[] POWER_STATE_NAMES = {"OFF", "SLEEP", "ON"};
private static final String TAG = "GPS";
private static final int HOOK_LIBGPS = 1;
private static final int HOOK_GPS_STATUS_LISTENER = 2;
private static final int HOOK_NOTIFICATIONS = 4;
private static final int HOOK_TIMER = 8;
/* A named pipe written to by the hacked libgps library. */
private static String HOOK_GPS_STATUS_FILE = "/data/misc/gps.status";
private GpsStatus.Listener gpsListener;
private Thread statusThread;
private PowerNotifications notificationReceiver;
private Context context;
private LocationManager locationManager;
private GpsStatus lastStatus;
private boolean hasUidInfo;
private long sleepTime;
private long lastTime;
private GpsStateKeeper gpsState;
private SparseArray<GpsStateKeeper> uidStates;
private static final int GPS_STATUS_SESSION_BEGIN = 1;
private static final int GPS_STATUS_SESSION_END = 2;
private static final int GPS_STATUS_ENGINE_ON = 3;
private static final int GPS_STATUS_ENGINE_OFF = 4;
public GPS(Context context, PhoneConstants constants) {
this.context = context;
uidStates = new SparseArray<GpsStateKeeper>();
sleepTime = (long)Math.round(1000.0 * constants.gpsSleepTime());
hasUidInfo = NotificationService.available();
int hookMethod = 0;
final File gpsStatusFile = new File(HOOK_GPS_STATUS_FILE);
if(gpsStatusFile.exists()) {
/* The libgps hack appears to be available. Let's use this to gather
* our status updates from the GPS.
*/
hookMethod = HOOK_LIBGPS;
} else {
/* We can always use the status listener hook and perhaps the notification
* hook if we are running eclaire or higher and the notification hook
* is installed. We can only do this on eclaire or higher because it
* wasn't until eclaire that they fixed a bug where they didn't maintain
* a wakelock while the gps engine was on.
*/
hookMethod = HOOK_GPS_STATUS_LISTENER;
try {
if(NotificationService.available() &&
Integer.parseInt(Build.VERSION.SDK) >= 5 /* eclaire or higher */) {
hookMethod |= HOOK_NOTIFICATIONS;
}
} catch(NumberFormatException e) {
Log.w(TAG, "Could not parse sdk version: " + Build.VERSION.SDK);
}
}
/* If we don't have a way of getting the off<->sleep transitions through
* notifications let's just use a timer and simulat the state of the gps
* instead.
*/
if((hookMethod & (HOOK_LIBGPS | HOOK_NOTIFICATIONS)) == 0) {
hookMethod |= HOOK_TIMER;
}
/* Create the object that keeps track of the physical GPS state. */
gpsState = new GpsStateKeeper(hookMethod, sleepTime);
/* No matter what we are going to register a GpsStatus listener so that we
* can get the satellite count. Also if anything goes wrong with the
* libgps hook we will revert to using this.
*/
locationManager = (LocationManager)
context.getSystemService(Context.LOCATION_SERVICE);
gpsListener = new GpsStatus.Listener() {
public void onGpsStatusChanged(int event){
if(event == GpsStatus.GPS_EVENT_STARTED) {
gpsState.updateEvent(GPS_STATUS_SESSION_BEGIN,
HOOK_GPS_STATUS_LISTENER);
} else if(event == GpsStatus.GPS_EVENT_STOPPED) {
gpsState.updateEvent(GPS_STATUS_SESSION_END,
HOOK_GPS_STATUS_LISTENER);
}
synchronized(GPS.this) {
lastStatus = locationManager.getGpsStatus(lastStatus);
}
}
};
locationManager.addGpsStatusListener(gpsListener);
/* No matter what we register a notification service listener as well so
* that we can get uid information if it's available.
*/
if(hasUidInfo) {
notificationReceiver = new NotificationService.DefaultReceiver() {
public void noteStartWakelock(int uid, String name, int type) {
if(uid == SystemInfo.AID_SYSTEM &&
"GpsLocationProvider".equals(name)) {
gpsState.updateEvent(GPS_STATUS_ENGINE_ON, HOOK_NOTIFICATIONS);
}
}
public void noteStopWakelock(int uid, String name, int type) {
if(uid == SystemInfo.AID_SYSTEM &&
"GpsLocationProvider".equals(name)) {
gpsState.updateEvent(GPS_STATUS_ENGINE_OFF, HOOK_NOTIFICATIONS);
}
}
public void noteStartGps(int uid) {
updateUidEvent(uid, GPS_STATUS_SESSION_BEGIN, HOOK_NOTIFICATIONS);
}
public void noteStopGps(int uid) {
updateUidEvent(uid, GPS_STATUS_SESSION_END, HOOK_NOTIFICATIONS);
}
};
NotificationService.addHook(notificationReceiver);
}
if(gpsStatusFile.exists()) {
/* Start a thread to read from the named pipe and feed us status updates.
*/
statusThread = new Thread() {
public void run() {
try {
java.io.FileInputStream fin =
new java.io.FileInputStream(gpsStatusFile);
for(int event = fin.read(); !interrupted() && event != -1;
event = fin.read()) {
gpsState.updateEvent(event, HOOK_LIBGPS);
}
} catch(IOException e) {
e.printStackTrace();
}
if(!interrupted()) {
// TODO: Have this instead just switch to use different hooks.
Log.w(TAG, "GPS status thread exited. " +
"No longer gathering gps data.");
}
}
};
statusThread.start();
}
}
private void updateUidEvent(int uid, int event, int source) {
synchronized(uidStates) {
GpsStateKeeper state = uidStates.get(uid);
if(state == null) {
state = new GpsStateKeeper(HOOK_NOTIFICATIONS | HOOK_TIMER, sleepTime,
lastTime);
uidStates.put(uid, state);
}
state.updateEvent(event, source);
}
}
@Override
protected void onExit() {
if(gpsListener != null) {
locationManager.removeGpsStatusListener(gpsListener);
}
if(statusThread != null) {
statusThread.interrupt();
}
if(notificationReceiver != null) {
NotificationService.removeHook(notificationReceiver);
}
super.onExit();
}
@Override
public IterationData calculateIteration(long iteration) {
IterationData result = IterationData.obtain();
/* Get the number of satellites that were available in the last update. */
int satellites = 0;
synchronized(this) {
if(lastStatus != null) {
for(GpsSatellite satellite : lastStatus.getSatellites()) {
satellites++;
}
}
}
/* Get the power data for the physical gps device. */
GpsData power = GpsData.obtain();
synchronized(gpsState) {
double[] stateTimes = gpsState.getStateTimesLocked();
int curState = gpsState.getCurrentStateLocked();
power.init(stateTimes, curState == POWER_STATE_ON ? satellites : 0);
gpsState.resetTimesLocked();
}
result.setPowerData(power);
/* Get the power data for each uid if we have information on it. */
if(hasUidInfo) synchronized(uidStates) {
lastTime = beginTime + iterationInterval * iteration;
for(int i = 0; i < uidStates.size(); i++) {
int uid = uidStates.keyAt(i);
GpsStateKeeper state = uidStates.valueAt(i);
double[] stateTimes = state.getStateTimesLocked();
int curState = state.getCurrentStateLocked();
GpsData uidPower = GpsData.obtain();
uidPower.init(stateTimes, curState == POWER_STATE_ON ? satellites : 0);
state.resetTimesLocked();
result.addUidPowerData(uid, uidPower);
/* Remove state information for uids no longer using the gps. */
if(curState == POWER_STATE_OFF) {
uidStates.remove(uid);
i--;
}
}
}
return result;
}
@Override
public boolean hasUidInformation() {
return hasUidInfo;
}
/* This class is used to maintain the actual GPS state in addition to
* simulating individual uid states.
*/
private static class GpsStateKeeper {
private double[] stateTimes;
private long lastTime;
private int curState;
/* The sum of whatever hook sources are valid. See the HOOK_ constants. */
private int hookMask;
/* The time that the GPS hardware should turn off. This is only used
* if HOOK_TIMER is in the hookMask.
*/
private long offTime;
/* Gives the time that the GPS stays in the sleep state after the session
* has ended in milliseconds.
*/
private long sleepTime;
public GpsStateKeeper(int hookMask, long sleepTime) {
this(hookMask, sleepTime, SystemClock.elapsedRealtime());
}
public GpsStateKeeper(int hookMask, long sleepTime, long lastTime) {
this.hookMask = hookMask;
this.sleepTime = sleepTime; /* This isn't required if HOOK_TIEMR is not
* set. */
this.lastTime = lastTime;
stateTimes = new double[POWER_STATES];
curState = POWER_STATE_OFF;
offTime = -1;
}
/* Make sure that you have a lock on this before calling. */
public double[] getStateTimesLocked() {
updateTimesLocked();
/* Let's normalize the times so that power measurements are consistent. */
double total = 0;
for(int i = 0; i < POWER_STATES; i++) {
total += stateTimes[i];
}
if(total == 0) total = 1;
for(int i = 0; i < POWER_STATES; i++) {
stateTimes[i] /= total;
}
return stateTimes;
}
public void resetTimesLocked() {
for(int i = 0; i < POWER_STATES; i++) {
stateTimes[i] = 0;
}
}
public int getCurrentStateLocked() {
return curState;
}
/* Make sure that you have a lock on this before calling. */
private void updateTimesLocked() {
/* Update the time we were in the previous state. */
long curTime = SystemClock.elapsedRealtime();
/* Check if the GPS has gone to sleep as a result of a timer. */
if((hookMask & HOOK_TIMER) != 0 && offTime != -1 &&
offTime < curTime) {
stateTimes[curState] += (offTime - lastTime) / 1000.0;
curState = POWER_STATE_OFF;
offTime = -1;
}
/* Update the amount of time that we've been in the current state. */
stateTimes[curState] += (curTime - lastTime) / 1000.0;
lastTime = curTime;
}
/* When a hook source gets an event it should report it to updateEvent.
* The only exception is HOOK_TIMER which is handled within this class
* itself.
*/
public void updateEvent(int event, int source) {
synchronized(this) {
if((hookMask & source) == 0) {
/* We are not using this hook source, ignore. */
return;
}
updateTimesLocked();
int oldState = curState;
switch(event) {
case GPS_STATUS_SESSION_BEGIN:
curState = POWER_STATE_ON;
break;
case GPS_STATUS_SESSION_END:
if(curState == POWER_STATE_ON) {
curState = POWER_STATE_SLEEP;
}
break;
case GPS_STATUS_ENGINE_ON:
if(curState == POWER_STATE_OFF) {
curState = POWER_STATE_SLEEP;
}
break;
case GPS_STATUS_ENGINE_OFF:
curState = POWER_STATE_OFF;
break;
default:
Log.w(TAG, "Unknown GPS event captured");
}
if(curState != oldState) {
if(oldState == POWER_STATE_ON && curState == POWER_STATE_SLEEP) {
offTime = SystemClock.elapsedRealtime() + sleepTime;
} else {
/* Any other state transition should reset the off timer. */
offTime = -1;
}
}
}
}
}
@Override
public String getComponentName() {
return "GPS";
}
}