package com.samknows.measurement.environment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.json.JSONObject;
import org.w3c.dom.Element;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.util.Pair;
import com.samknows.libcore.SKPorting;
import com.samknows.measurement.SK2AppSettings;
import com.samknows.measurement.SKApplication;
import com.samknows.measurement.schedule.ScheduleConfig.LocationType;
import com.samknows.measurement.storage.PassiveMetric;
import com.samknows.measurement.TestRunner.TestContext;
import com.samknows.measurement.storage.StorageTestResult;
import com.samknows.measurement.util.OtherUtils;
import com.samknows.measurement.util.XmlUtils;
import com.samknows.tests.SKAbstractBaseTest;
public class LocationDataCollector extends BaseDataCollector implements LocationListener {
static final String TAG = "LocationDataCollector";
private static final long serialVersionUID = 1L;
private long time;
private long listenerDelay;
// private float listenerMinDst;
private boolean getLastKnown;
private transient List<Location> mLocations;
Location mLastLocation = null;
// There is more than one way to obtain Location on Android.
// - The Android Location API (LOCATION_SERVICE - android.location.*)
// - Google Play Services Location API (com.google.android.gms.location.*)
// We use the first of these two options, as then there is no dependency on
// Google Play Services...
// See http://www.rahuljiresal.com/2014/02/user-location-on-android/ for some
// useful background on this.
private transient LocationManager manager;
private boolean gotLastLocation = false;
private LocationType locationType;
static private Location sLastKnown = null;
private int mProviderStatus = LocationProvider.AVAILABLE;
public static void sForceFastLocationCheck(boolean canUseGPS) {
Context context = SKApplication.getAppInstance().getApplicationContext();
LocationManager manager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (manager == null) {
SKPorting.sAssert(LocationDataCollector.class, false);
return;
}
LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
//Log.d("LOCATION", "CHANGED! Lat="+location.getLatitude() + ", Lng:" + location.getLongitude());
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
};
// http://stackoverflow.com/questions/10405277/requestsingleupdate-doesnt-automatically-fetch-gps-location
// Network location is fast and cheap (in terms of battery use) and a good strategy is to use network location until you get GPS.
// However, GPS is faster.
if (canUseGPS && manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
manager.requestSingleUpdate(LocationManager.GPS_PROVIDER, locationListener, Looper.getMainLooper());
} else if (manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
manager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, locationListener, Looper.getMainLooper());
} else {
// Don't do this, as it annoys the unit tests! SKLogger.sAssert(OtherUtils.isThisDeviceAnEmulator());
}
}
public static Pair<Location,LocationType> sGetLastKnownLocation() {
Context context = SKApplication.getAppInstance().getApplicationContext();
LocationManager manager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (manager == null) {
SKPorting.sAssert(LocationDataCollector.class, false);
return null;
}
if (SK2AppSettings.sHasPermission("android.permission.ACCESS_FINE_LOCATION")) {
if (manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
try {
Location lastKnownLocation = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
// This MIGHT be null!
if (lastKnownLocation != null) {
sLastKnown = lastKnownLocation;
//Log.d("GPS LOCATION", "lastKnownLocation Lat="+sLastKnown.getLatitude() + ", Lng:" + sLastKnown.getLongitude());
return new Pair<>(lastKnownLocation, LocationType.gps);
}
} catch (Exception e) {
SKPorting.sAssert(false);
}
}
}
if (manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
try {
Location lastKnownLocation = manager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
// This MIGHT be null!
if (lastKnownLocation != null) {
sLastKnown = lastKnownLocation;
//Log.d("WI-FI LOCATION", "lastKnownLocation Lat="+sLastKnown.getLatitude() + ", Lng:" + sLastKnown.getLongitude());
return new Pair<>(lastKnownLocation, LocationType.network);
}
} catch (Exception e) {
SKPorting.sAssert(false);
}
}
return null;
}
static public List<JSONObject> sGetPassiveLocationMetric(boolean forceReportLastKnownAsLocation) {
Pair<Location, LocationType> lastKnownPair = sGetLastKnownLocation();
if (lastKnownPair == null) {
// Nothing known - don't store a passive metric, simply return empty instead...
//SKPorting.sAssert(OtherUtils.isThisDeviceAnEmulator());
return new ArrayList<>();
}
Location lastKnownLocation = lastKnownPair.first;
if (lastKnownLocation == null) {
// Nothing known - don't store a passive metric, simply return empty...
return new ArrayList<>();
}
LocationType lastKnownLocationType = lastKnownPair.second;
boolean bReportAsLastKnownLocation = true;
if (forceReportLastKnownAsLocation == true) {
bReportAsLastKnownLocation = false;
}
LocationData locationData = new LocationData(bReportAsLastKnownLocation, lastKnownLocation, lastKnownLocationType);
if (forceReportLastKnownAsLocation == true) {
locationData.setLocationTimeMilli(SKAbstractBaseTest.sGetUnixTimeStampMilli());
}
// The following should only ever return a List<JSONObject> containing one item!
List<JSONObject> passiveMetrics = locationData.convertToJSON();
int items = passiveMetrics.size();
SKPorting.sAssert(StorageTestResult.class, items == 1);
return passiveMetrics;
}
@Override
public void start(TestContext tc) {
super.start(tc);
mLocations = Collections.synchronizedList(new ArrayList<Location>());
manager = (LocationManager) tc.getSystemService(Context.LOCATION_SERVICE);
if (manager == null) {
SKPorting.sAssert(getClass(), false);
return;
}
locationType = SK2AppSettings.getSK2AppSettingsInstance().getLocationServiceType();
if (locationType == null) {
SKPorting.sAssert(false);
return;
}
//if the provider in the settings is gps but the service is not enable fail over to network provider
if (locationType == LocationType.gps && !manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
// The following call has been seen to return null on some devices!
List<String> providers = manager.getAllProviders();
if (providers != null) {
if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
locationType = LocationType.network;
}
} else {
SKPorting.sAssert(false);
}
}
if (locationType != LocationType.gps && locationType != LocationType.network) {
// Rather than simply crashing the app with an exception - stick to Network type, which will
// be handled benignly...
locationType = LocationType.network;
}
String provider = locationType == LocationType.gps ? LocationManager.GPS_PROVIDER : LocationManager.NETWORK_PROVIDER;
if (getLastKnown) {
Location tryLocation = manager.getLastKnownLocation(provider);
if (tryLocation != null) {
sLastKnown = tryLocation;
}
}
gotLastLocation = false;
// On some devices, this can throw an exception, of the form:
// java.lang.IllegalArgumentException: provider doesn't exist: network
// or (sic!):
// java.lang.IllegalArgumentException: provider doesn't exist: null
// We must not allow that behavior to cause the app to crash.
try {
manager.requestLocationUpdates(provider, 0, 0, LocationDataCollector.this, Looper.getMainLooper());
Log.d(TAG, "start collecting location data from: " + provider);
} catch (java.lang.IllegalArgumentException ex) {
SKPorting.sAssert(getClass(), false);
}
try {
Log.d(TAG, "sleeping: " + time);
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
//stop listening for location updates if we are on network. That is done because network location uses network and breaks NetworkCondition
if (locationType == LocationType.network) {
manager.removeUpdates(this);
}
}
@Override
public void clearData(){
if(mLocations != null ){
mLocations.clear();
}
}
/**
* returns true if got location
* @param time
* @return
*/
public synchronized boolean waitForLocation(long time) {
if (!gotLastLocation) {
try {
wait(time);
} catch (InterruptedException e) {
SKPorting.sAssertE(this, "Interruption while waiting for location", e);
}
}
return gotLastLocation;
}
@Override
public void stop(TestContext ctx) {
if(isEnabled){
super.stop(ctx);
manager.removeUpdates(this);
lastReceivedTime = -1;
Log.d(TAG, "location datas: " + mLocations.size());
Log.d(TAG, "stop collecting location data");
}else{
Log.d(TAG, "LocationDataCollector is not enabled");
}
}
private long lastReceivedTime=-1;
@Override
public synchronized void onLocationChanged(Location location) {
//Log.d(TAG, "received new location");
if (location != null) {
long timeDiff = System.currentTimeMillis() - lastReceivedTime;
if (lastReceivedTime == -1 || timeDiff > listenerDelay) {
lastReceivedTime = System.currentTimeMillis();
synchronized(this) {
mLocations.add(location);
}
mLastLocation = location;
gotLastLocation = true;
notifyAll();
}
}
}
// @Override
// public List<String> getOutput() {
// List<String> list = new ArrayList<String>();
// synchronized(this) {
// for(Location l: mLocations){
// list.addAll(new LocationData(l,locationType).convert());
// }
// }
// return list;
// }
@Override
public List<JSONObject> getPassiveMetric() {
List<JSONObject> ret = new ArrayList<>();
if(sLastKnown != null){
ret.addAll(locationToPassiveMetric(sLastKnown));
}
synchronized(this) {
for(Location l: mLocations){
ret.addAll(locationToPassiveMetric(l));
}
}
return ret;
}
//Receive a Location object and returns a JSONObject ready to be inserted in the database
//and displayed to the interface
private List<JSONObject> locationToPassiveMetric(Location loc){
List<JSONObject> ret = new ArrayList<>();
ret.add(PassiveMetric.create(PassiveMetric.METRIC_TYPE.LOCATIONPROVIDER, loc.getTime(), locationType+""));
ret.add(PassiveMetric.create(PassiveMetric.METRIC_TYPE.LATITUDE, loc.getTime(), String.format("%1.5f", loc.getLatitude())));
ret.add(PassiveMetric.create(PassiveMetric.METRIC_TYPE.LONGITUDE, loc.getTime(), String.format("%1.5f", loc.getLongitude())));
ret.add(PassiveMetric.create(PassiveMetric.METRIC_TYPE.ACCURACY, loc.getTime(), loc.getAccuracy()+" m"));
// LocationData locationData = new LocationData(loc, LocationType.gps);
// ret.add(PassiveMetric.create(PassiveMetric.METRIC_TYPE.MUNICIPALITY, loc.getTime(), locationData.mMuncipality));
// ret.add(PassiveMetric.create(PassiveMetric.METRIC_TYPE.COUNTRYNAME, loc.getTime(), locationData.mCountryName));
return ret;
}
//Used to che
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
mProviderStatus = status;
}
@Override
public void onProviderEnabled(String provider) {
Log.d(TAG, "onProviderEnabled: " + provider);
}
@Override
public void onProviderDisabled(String provider) {
Log.d(TAG, "onProviderDisabled: "+provider);
}
//---------------------------------------------------------
public static BaseDataCollector parseXml(Element node) {
LocationDataCollector c = new LocationDataCollector();
String time = node.getAttribute("time");
c.time = XmlUtils.convertTime(time);
String listenerDelay = node.getAttribute("listenerDelay");
c.listenerDelay = XmlUtils.convertTime(listenerDelay);
// c.listenerMinDst = Float.valueOf(node.getAttribute("listenerMinDistance"));
c.getLastKnown = Boolean.parseBoolean(node.getAttribute("lastKnown"));
return c;
}
@Override
public List<JSONObject> getJSONOutput(){
List<JSONObject> ret = new ArrayList<>();
if(getLastKnown && sLastKnown != null){
ret.addAll(new LocationData(true, sLastKnown, locationType).convertToJSON());
}
synchronized(this) {
for(Location l: mLocations){
ret.addAll((new LocationData(l, locationType)).convertToJSON());
}
}
return ret;
}
public List<DCSData> getPartialData(){
List<DCSData> ret = new ArrayList<>();
synchronized(this){
if(getLastKnown && sLastKnown != null){
ret.add(new LocationData(true, sLastKnown, locationType));
sLastKnown = null;
}
if( mLocations.isEmpty() && mLastLocation != null){
ret.add(new LocationData(mLastLocation, locationType, mProviderStatus));
}
for(Location l: mLocations){
ret.add(new LocationData(l, locationType));
}
mLocations.clear();
}
return ret;
}
}