/*
This file is part of Project MAXS.
MAXS and its modules 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.
MAXS 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 MAXS. If not, see <http://www.gnu.org/licenses/>.
*/
package org.projectmaxs.module.locationfine.service;
import java.util.LinkedList;
import java.util.List;
import org.projectmaxs.module.locationfine.ModuleService;
import org.projectmaxs.module.locationfine.service.gpsenabler.GpsEnablerDisabler;
import org.projectmaxs.module.locationfine.service.gpsenabler.PowerWidgetFlaw;
import org.projectmaxs.shared.global.GlobalConstants;
import org.projectmaxs.shared.global.Message;
import org.projectmaxs.shared.global.messagecontent.Element;
import org.projectmaxs.shared.global.messagecontent.Text;
import org.projectmaxs.shared.global.util.DateTimeUtil;
import org.projectmaxs.shared.global.util.Log;
import org.projectmaxs.shared.mainmodule.Command;
import org.projectmaxs.shared.module.MainUtil;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
public class LocationService extends Service {
public static final String START_SERVICE = ModuleService.LOCATIONFINE_MODULE_PACKAGE
+ ".LOCATION_START_SERVICE";
public static final String STOP_SERVICE = ModuleService.LOCATIONFINE_MODULE_PACKAGE
+ ".LOCATION_STOP_SERVICE";
/**
* Interval in milliseconds after which a new location is always considered better.
*
* Currently 2 minutes
*/
private static final int UPDATE_INTERVAL = 1000 * 60 * 2;
private static final Log LOG = Log.getLog();
private static final List<GpsEnablerDisabler> GPS_ENABLER_DISABLERS = new LinkedList<GpsEnablerDisabler>();
static {
GPS_ENABLER_DISABLERS.add(new PowerWidgetFlaw());
}
private LocationListener mLocationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
LOG.d("onLocationChanged: locaction=" + location);
if (isBetterLocation(location)) send(location);
}
@Override
public void onProviderDisabled(String provider) {}
@Override
public void onProviderEnabled(String provider) {}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {}
};
private LocationManager mLocationManager;
private Location mCurrentBestLocation;
private List<String> mAllProviders;
private Command mCommand = new Command();
private boolean mGpsManuallyEnabled = false;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
mAllProviders = mLocationManager.getAllProviders();
}
// TODO module-locationfine uses a 'dangerous' permission, which requires as of Android 6.0 (23)
// or higher a the user's grant on runtime (additional to the permission on install time).
// Implement this.
@SuppressLint("MissingPermission")
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) intent = new Intent(START_SERVICE);
mCommand = intent.getParcelableExtra(GlobalConstants.EXTRA_COMMAND);
final String action = intent.getAction();
LOG.d("onStartCommand: action=" + action + ", flags=" + flags + ", startId=" + startId);
if (START_SERVICE.equals(action)) {
if (!tryEnableGps()) send(new Message("GPS was disabled and we could not enable it."));
for (String provider : mAllProviders)
mLocationManager.requestLocationUpdates(provider, 1000 * 30, 5, mLocationListener);
for (String provider : mAllProviders) {
Location location = mLocationManager.getLastKnownLocation(provider);
if (location != null && isBetterLocation(location)) {
send(location);
break;
}
}
} else if (STOP_SERVICE.equals(action)) {
mLocationManager.removeUpdates(mLocationListener);
if (mGpsManuallyEnabled) {
// Only reset to false if disabling GPS was successful
if (tryDisableGps()) mGpsManuallyEnabled = false;
}
stopSelfResult(startId);
} else {
throw new IllegalStateException("Unknown action: " + action);
}
return START_STICKY;
}
private boolean gpsEnabled() {
return mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}
private boolean tryEnableGps() {
if (gpsEnabled()) return true;
for (GpsEnablerDisabler enabler : GPS_ENABLER_DISABLERS) {
try {
enabler.enableGps(this);
} catch (Throwable e) {
LOG.d("tryEnableGps", e);
}
if (gpsEnabled()) {
mGpsManuallyEnabled = true;
return true;
}
}
return false;
}
private boolean tryDisableGps() {
if (!gpsEnabled()) return true;
for (GpsEnablerDisabler enabler : GPS_ENABLER_DISABLERS) {
try {
enabler.disableGps(this);
} catch (Throwable e) {
LOG.d("tryDisableGps", e);
}
if (!gpsEnabled()) return true;
}
return false;
}
private void send(Location location) {
mCurrentBestLocation = location;
String latitude = Double.toString(location.getLatitude());
String longitude = Double.toString(location.getLongitude());
String time = Long.toString(location.getTime());
String humanTime = DateTimeUtil.shortFromUtc(location.getTime());
String accuracy = location.hasAccuracy() ? Float.toString(location.getAccuracy()) : null;
String altitude = location.hasAltitude() ? Double.toString(location.getAltitude()) : null;
String speed = location.hasSpeed() ? Float.toString(location.getSpeed()) : null;
Message message = new Message();
Text text = new Text();
text.addBoldNL("Location (" + humanTime + ')');
text.addNL("Latitude: " + latitude + " Longitude: " + longitude);
text.addNL("http://www.openstreetmap.org/?mlat=" + latitude + "&mlon=" + longitude
+ "&zoom=14&layers=M");
if (accuracy != null) text.addNL("Accuracy: " + accuracy);
if (altitude != null) text.addNL("Altitude: " + altitude);
if (speed != null) text.addNL("Speed: " + speed);
message.add(text);
// Add a non human-readable element with that information
Element element = new Element("location");
element.addChildElement(new Element("latitude", latitude));
element.addChildElement(new Element("longitude", longitude));
element.addChildElement(new Element("time", time));
if (accuracy != null) element.addChildElement(new Element("accuracy", accuracy));
if (altitude != null) element.addChildElement(new Element("altitude", altitude));
if (speed != null) element.addChildElement(new Element("speed", speed));
message.add(element);
send(message);
}
private void send(Message message) {
message.setId(mCommand.getId());
MainUtil.send(message, this);
}
@TargetApi(17)
private boolean isBetterLocation(Location newLocation) {
if (mCurrentBestLocation == null) return true;
long timeDelta;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
timeDelta = newLocation.getElapsedRealtimeNanos()
- mCurrentBestLocation.getElapsedRealtimeNanos();
} else {
timeDelta = newLocation.getTime() - mCurrentBestLocation.getTime();
}
if (timeDelta < 0) {
// The new location is older then the current best location, therefore it's not a better
// location
LOG.w("new location older then current best location");
return false;
}
boolean newLocationIsSignificantlyNewer = timeDelta > UPDATE_INTERVAL;
float accuracyDelta = newLocation.getAccuracy() - mCurrentBestLocation.getAccuracy();
boolean newLocationIsMoreAccurate = accuracyDelta < 0;
if (newLocationIsMoreAccurate) return true;
// We consider two locations to have the same accuracy if the differ only within 5 meters
boolean newLocationHasSameAccurary = Math.abs(accuracyDelta) < 5;
boolean newLocationIsSameAsCurrent = newLocationHasSameAccurary
&& newLocation.getAltitude() == mCurrentBestLocation.getAltitude()
&& newLocation.getLongitude() == mCurrentBestLocation.getLongitude();
if (!newLocationIsSameAsCurrent
&& (newLocationIsSignificantlyNewer || newLocationHasSameAccurary)) return true;
return false;
}
}