/*
* Copyright (C) 2012 The Serval Project
*
* This file is part of the Serval Maps Software
*
* Serval Maps Software 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 source code 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 source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.servalproject.maps.location;
import java.util.TimeZone;
import org.servalproject.maps.ServalMaps;
import org.servalproject.maps.protobuf.BinaryFileWriter;
import org.servalproject.maps.provider.LocationsContract;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.SQLException;
import android.location.Location;
import android.location.LocationListener;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
/**
* Used to listen for location updates and store them in the database
*/
public class LocationCollector implements LocationListener {
/*
* class level constants
*/
private boolean V_LOG = false;
private String TAG = "LocationCollector";
/*
* class level variables
*/
private Context context;
/**
* the most recent and most accurate location information
*/
private static volatile Location currentLocation = null;
private String timeZone = TimeZone.getDefault().getID();
private String phoneNumber;
private String subscriberId;
private ContentResolver contentResolver;
public LocationCollector(Context context) {
super();
if(context == null) {
throw new IllegalArgumentException("the context parameter is required");
}
ServalMaps mApplication = (ServalMaps) context.getApplicationContext();
phoneNumber = mApplication.getPhoneNumber();
subscriberId = mApplication.getSid();
mApplication = null;
contentResolver = context.getContentResolver();
this.context = context;
}
/**
* get the most recent and most accurate location information
*/
public static Location getLocation() {
return currentLocation;
}
/*
* Called when the location has changed.
* (non-Javadoc)
* @see android.location.LocationListener#onLocationChanged(android.location.Location)
*/
@Override
public void onLocationChanged(Location location) {
// process this new location
if(V_LOG) {
Log.v(TAG, "new location received");
}
// check to see if this location is better than the one we have already
if(isBetterLocation(location, currentLocation) == true) {
if(V_LOG) {
Log.v(TAG, "new location is better than current location");
if(currentLocation != null) {
Log.v(TAG, "old location: Lat: " + currentLocation.getLatitude() + " Lng: " + currentLocation.getLongitude() + " accuracy: " + currentLocation.getAccuracy());
}
Log.v(TAG, "new location: Lat: " + location.getLatitude() + " Lng: " + location.getLongitude() + " accuracy: " + location.getAccuracy());
}
// save the location for later
currentLocation = location;
if(phoneNumber == null || subscriberId == null) {
// these may be null but will be populated once the
// sticky to Serval Mesh returns
return;
}
long mTime = System.currentTimeMillis();
ContentValues mNewValues = new ContentValues();
mNewValues.put(LocationsContract.Table.PHONE_NUMBER, phoneNumber);
mNewValues.put(LocationsContract.Table.SUBSCRIBER_ID, subscriberId);
mNewValues.put(LocationsContract.Table.LATITUDE, location.getLatitude());
mNewValues.put(LocationsContract.Table.LONGITUDE, location.getLongitude());
mNewValues.put(LocationsContract.Table.TIMEZONE, timeZone);
mNewValues.put(LocationsContract.Table.TIMESTAMP, mTime);
try {
Uri newRecord = contentResolver.insert(LocationsContract.CONTENT_URI, mNewValues);
if(V_LOG) {
Log.v(TAG, "new location record created with id: " + newRecord.getLastPathSegment());
}
// functionality not required at this stage
//OutgoingMeshMS.sendLocationMessage(context, newRecord.getLastPathSegment());
// write an entry to the binary log file
BinaryFileWriter.writeLocation(context, newRecord.getLastPathSegment());
}catch (SQLException e) {
Log.e(TAG, "unable to add new location record", e);
}
} else {
if(V_LOG) {
Log.v(TAG, "new location is not better than current location");
}
}
}
/*
* Called when the provider is disabled by the user.
* (non-Javadoc)
* @see android.location.LocationListener#onProviderDisabled(java.lang.String)
*/
@Override
public void onProviderDisabled(String arg0) {
// TODO Auto-generated method stub
}
/*
* Called when the provider is enabled by the user.
* (non-Javadoc)
* @see android.location.LocationListener#onProviderEnabled(java.lang.String)
*/
@Override
public void onProviderEnabled(String arg0) {
// TODO Auto-generated method stub
}
/*
* Called when the provider status changes.
* (non-Javadoc)
* @see android.location.LocationListener#onStatusChanged(java.lang.String, int, android.os.Bundle)
*/
@Override
public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
// TODO Auto-generated method stub
}
/*
* The following two methods are sourced from:
* http://developer.android.com/guide/topics/location/obtaining-user-location.html#BestPerformance
*
* They are used under the terms of the Apache 2.0 license
* http://www.apache.org/licenses/LICENSE-2.0
*/
private static final int TWO_MINUTES = 1000 * 60 * 2; // maximum allowed time difference
private static final int THIRTY_SECONDS = 1000 * 30; // minimum allowed time difference
private static final int MAXIMUM_DEVIATION = 200; // in meters
/** Determines whether one Location reading is better than the current Location fix
* @param location The new Location that you want to evaluate
* @param currentBestLocation The current Location fix, to which you want to compare the new one
*/
private boolean isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) {
// A new location is always better than no location
return true;
}
// Check whether the new location fix is newer or older
long timeDelta = location.getTime() - currentBestLocation.getTime();
boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
boolean isNewer = timeDelta > THIRTY_SECONDS;
// If it's been more than two minutes since the current location, use the new location
// because the user has likely moved
if (isSignificantlyNewer) {
if(V_LOG) {
Log.v(TAG, "new location is significantly newer");
Log.v(TAG, "time delta: " + timeDelta + " comparison: " + TWO_MINUTES);
}
return true;
// If the new location is more than two minutes older, it must be worse
} else if (isSignificantlyOlder) {
if(V_LOG) {
Log.v(TAG, "new location is significantly olrder");
Log.v(TAG, "time delta: " + timeDelta + " comparison: " + TWO_MINUTES);
}
return false;
}
// Check whether the new location fix is more or less accurate
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
boolean isLessAccurate = accuracyDelta > 0;
boolean isMoreAccurate = accuracyDelta < 0;
boolean isSignificantlyLessAccurate = accuracyDelta > MAXIMUM_DEVIATION;
// Check if the old and new location are from the same provider
boolean isFromSameProvider = isSameProvider(location.getProvider(),
currentBestLocation.getProvider());
// Determine location quality using a combination of timeliness and accuracy
if (isMoreAccurate) {
if(V_LOG) {
Log.v(TAG, "new location is more accurate");
Log.v(TAG, "accuracy delta: " + accuracyDelta);
}
return true;
} else if (isNewer && !isLessAccurate) {
if(V_LOG) {
Log.v(TAG, "new location is newer and not less acurrate");
Log.v(TAG, "time delta: " + timeDelta + " comparison: " + TWO_MINUTES);
Log.v(TAG, "accuracy delta: " + accuracyDelta);
}
return true;
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
if(V_LOG) {
Log.v(TAG, "new location is newer and not significantly less acurrate and is from the same provider");
Log.v(TAG, "time delta: " + timeDelta + " comparison: " + TWO_MINUTES);
Log.v(TAG, "accuracy delta: " + accuracyDelta + " threshold: " + MAXIMUM_DEVIATION);
}
return true;
}
return false;
}
/** Checks whether two providers are the same */
private boolean isSameProvider(String provider1, String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}
}