Edit: I'm rewriting this question because I apparently wasn't clear.
Sometimes the GPS service on Android phones takes a long time to get a fix. Sometimes it's fast, sometimes it takes hours. I know and accept this.
I have an application that does many things. One of the things it must do is allow the user to click a button to send their current coordinates to a server. What I need are the coordinates of the phone when the user clicks the button or within a reasonably short time thereafter.
Because I know that getting a GPS fix is not instant and I know that it could take minutes or hours (during which the user has moved a great distance), I need to code a timeout for this feature. For this feature, it is simply not acceptable to upload the GPS location of the user three minutes (for example) after they clicked the button. It's fine if it takes 45 seconds, not okay if it takes 75 seconds. It's fine to give the user an error notification if the feature failed to get a location fast enough.
I need a feature to 'get the GPS location and send it to the server, unless it takes more than one minute'.
My original code is below. I have changed some things since posting it. I have added a Timer in the onStartCommand() method. I start a TimerTask that after 60 seconds will call my stop() method. At the beginning of the onLocationChanged() method, I cancel the TimerTask.
My question is: Is the Timer scheme a good way of implementing this timeout? Is there a better way?
Original question:
I'm writing an Android application that, among other things, needs to send the current GPS coordinates to a server when the user tells it to. From a context menu, I run the service below. The service is a LocationListener and requests updates from the LocationManager. When it gets a location (onLocationChanged()), it removes itself as a listener and sends the coordinates off to the server. All of this is working.
However, if GPS coordinates are not quickly available, my service just keeps running until it gets some. It holds up the UI with a progress dialog, which is annoying. Worse, if the user has moved since starting the service, the first GPS coordinates might be wrong and the app will send bad data to the server.
I need a timeout on the service. Is there a good way to do that? I'm not very experienced with threads. I think I can run a Runnable in the onStartCommand() method that will somehow count down 30 seconds and then, if there is no GPS result yet, call my service's stop() method. Does that sound like the best way to do this?
Alternatively, is it possible to tell if the GPS cannot get a fix? How would I go about doing that?
Edit: To further clarify, I'm looking for the best way to "give up" on getting a Location after some amount of time.
public class AddCurrentLocation extends Service implements LocationListener {
Application app;
LocationManager mLocManager;
ProgressDialog mDialog;
#Override
public int onStartCommand(Intent intent, int arg0, int arg1) {
app = getApplication();
// show progress dialog
if (app.getScreen() != null) {
mDialog = ProgressDialog.show(app.getScreen(), "", "Adding Location. Please wait...", true);
}
// find GPS service and start listening
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
mLocManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
String bestProvider = mLocManager.getBestProvider(criteria, true);
mLocManager.requestLocationUpdates(bestProvider, 2000, 0, this);
return START_NOT_STICKY;
}
private void stop() {
mLocManager.removeUpdates(this);
if (mDialog != null) {
mDialog.dismiss();
}
stopSelf();
}
#Override
public void onLocationChanged(Location location) {
// done with GPS stop listening
mLocManager.removeUpdates(this);
sendLocation(location); // method to send info to server
stop();
}
// other required methods and sendLocation() ...
}
That's not really how it works. It will consistently take that long in most situations to get a GPS fix. But from that point on each update (every 2 sec in your code)will be the person's current position. And the first fix you get will be the person's current position, so the data will not be "out of date".
Another thing. If you are running this code in a service you shouldn't block the UI with a progress dialog and definitely not from the Service. That is a memory leak waiting to happen. You should only show progress if it is something that might take 5 sec at the most and is running in a thread in the Activity. Another option is to show the progress dialog in the title bar, and still let the user interact with the app (which is why you use a service anyway). Showing progresses for a long period of time really isn't that User Friendly. Especially if they somehow change orientation (maybe on accident) and then your app crashes because of the service handle to the dialog and they have to start over.
Take a look at the Google I/O 2010 app to see a great example of how an activity should work with a service. It uses a service to pull back data, and shows a progress in the title while the service is doing some work. And still lets you do other things in the app.
Scott, there are many factors that affect how long a first fix can take - or even whether a fix can be achieved, the most common being physical obstacles between the device and satellites (sucha s buildings, canyon walls, etc).
You can't control how long it takes for the GPS engine to deliver a fix, but you can tell how its doing, including time of first fix:
locationManager.addGpsStatusListener(gpsListener);
// this reports on the status of the GPS engine, but does not enable additional controls
private static final GpsStatus.Listener gpsListener = new GpsStatus.Listener() {
public void onGpsStatusChanged(int event) {
GpsStatus gpsStatus = locationManager.getGpsStatus(null);
switch (event) {
case GpsStatus.GPS_EVENT_STARTED:
Log.i(TAG, "onGpsStatusChanged(): GPS started");
break;
case GpsStatus.GPS_EVENT_FIRST_FIX:
Log.i(TAG, "onGpsStatusChanged(): time to first fix in ms = " + gpsStatus.getTimeToFirstFix());
break;
case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
// int maxSatellites = gpsStatus.getMaxSatellites(); // appears fixed at 255
// if (H.DEBUG) Log.d(TAG, "onGpsStatusChanged(): max sats = " + maxSatellites);
if (H.VERBOSE) Log.d(TAG, "onGpsStatusChanged(): ##,used,s/n,az,el");
Iterable<GpsSatellite>satellites = gpsStatus.getSatellites();
Iterator<GpsSatellite>satI = satellites.iterator();
while (satI.hasNext()) {
GpsSatellite satellite = satI.next();
if (H.VERBOSE) Log.d(TAG, "onGpsStatusChanged(): " + satellite.getPrn() + "," + satellite.usedInFix() + "," + satellite.getSnr() + "," + satellite.getAzimuth() + "," + satellite.getElevation());
// http://en.wikipedia.org/wiki/Global_Positioning_System: the almanac consists of coarse orbit and status information for each satellite
// http://en.wikipedia.org/wiki/Ephemeris: the positions of astronomical objects in the sky at a given time
// + "," + satellite.hasAlmanac() + "," + satellite.hasEphemeris());
}
break;
case GpsStatus.GPS_EVENT_STOPPED:
Log.i(TAG, "onGpsStatusChanged(): GPS stopped");
break;
}
}
};
Events will be generated as the engine attempts to listen to available satellites. On a recent test of this with light obstacles I found it took 22.4 seconds to get an initial fix, during which 24 SATELLITE_STATUS events reporting the gradual access of 8 satellites before sufficiently clean signals were received to achieve the fix. Here is the last event:
06-08 23:23:25.147,D,GPS,22427,"onGpsStatusChanged(): ##,used,s/n,az,el"
06-08 23:23:25.147,D,GPS,22427,"onGpsStatusChanged(): 2,true,26.0,57.0,73.0"
06-08 23:23:25.147,D,GPS,22427,"onGpsStatusChanged(): 4,true,30.0,46.0,27.0"
06-08 23:23:25.147,D,GPS,22427,"onGpsStatusChanged(): 5,true,19.0,144.0,25.0"
06-08 23:23:25.155,D,GPS,22427,"onGpsStatusChanged(): 9,true,22.0,202.0,22.0"
06-08 23:23:25.155,D,GPS,22427,"onGpsStatusChanged(): 10,true,17.0,109.0,32.0"
06-08 23:23:25.155,D,GPS,22427,"onGpsStatusChanged(): 12,true,32.0,320.0,80.0"
06-08 23:23:25.155,D,GPS,22427,"onGpsStatusChanged(): 29,true,21.0,278.0,21.0"
06-08 23:23:25.155,D,GPS,22427,"onGpsStatusChanged(): 30,true,31.0,312.0,43.0"
06-08 23:23:25.163,D,GpsLocationProvider,1039,TTFF: 22457
06-08 23:23:25.194,I,GPS,22427,onGpsStatusChanged(): time to first fix in ms = 22457
Note that at fix time, you will be getting the current location, not where you once may have been. I think along with what you have already you can get there now. Or, check out how the pros do it here.
There is a example about get GPS location with timeout.
http://sikazi.blogspot.com/2010/09/android-gps-timeout.html#more
I have been struggling with a similar issue and recently switched from a timer to an AlarmManager, which appears to be much more robust. That might be overkill for your situation (I am using this for repeated location sampling), but you might want to at least use a Handler instead of the timer. (Use Handler.postDelayed.)
Related
I try to access the accelerometer from the NDK. So far it works. But the way events are written to the eventqueue seems a little bit strange.
See the following code:
ASensorManager* AcquireASensorManagerInstance(void) {
typedef ASensorManager *(*PF_GETINSTANCEFORPACKAGE)(const char *name);
void* androidHandle = dlopen("libandroid.so", RTLD_NOW);
PF_GETINSTANCEFORPACKAGE getInstanceForPackageFunc = (PF_GETINSTANCEFORPACKAGE) dlsym(androidHandle, "ASensorManager_getInstanceForPackage");
if (getInstanceForPackageFunc) {
return getInstanceForPackageFunc(kPackageName);
}
typedef ASensorManager *(*PF_GETINSTANCE)();
PF_GETINSTANCE getInstanceFunc = (PF_GETINSTANCE) dlsym(androidHandle, "ASensorManager_getInstance");
return getInstanceFunc();
}
void init() {
sensorManager = AcquireASensorManagerInstance();
accelerometer = ASensorManager_getDefaultSensor(sensorManager, ASENSOR_TYPE_ACCELEROMETER);
looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
accelerometerEventQueue = ASensorManager_createEventQueue(sensorManager, looper, LOOPER_ID_USER, NULL, NULL);
auto status = ASensorEventQueue_enableSensor(accelerometerEventQueue,
accelerometer);
status = ASensorEventQueue_setEventRate(accelerometerEventQueue,
accelerometer,
SENSOR_REFRESH_PERIOD_US);
}
That's how I initialize everything. My SENSOR_REFRESH_PERIOD_US is 100.000 - so 10 refreshs per second. Now I have the following method to receive the events of the event queue.
vector<sensorEvent> update() {
ALooper_pollAll(0, NULL, NULL, NULL);
vector<sensorEvent> listEvents;
ASensorEvent event;
while (ASensorEventQueue_getEvents(accelerometerEventQueue, &event, 1) > 0) {
listEvents.push_back(sensorEvent{event.acceleration.x, event.acceleration.y, event.acceleration.z, (long long) event.timestamp});
}
return listEvents;
}
sensorEvent at this point is a custom struct which I use. This update method gets called via JNI from Android every 10 seconds from an IntentService (to make sure it runs even when the app itself is killed). Now I would expect to receive 100 values (10 per second * 10 seconds). In different tests I received around 130 which is also completly fine for me even it's a bit off. Then I read in the documentation of ASensorEventQueue_setEventRate that it's not forced to follow the given refresh period. So if I would get more than I wanted it would be totally fine.
But now the problem: Sometimes I receive like 13 values in 10 seconds and when I continue to call update 10 secods later I get the 130 values + the missing 117 of the run before. This happens completly random and sometimes it's not the next run but the fourth following or something like that.
I am completly fine with being off from the refresh period by having more values. But can anyone explain why it happens that there are so many values missing and they appear 10 seconds later in the next run? Or is there maybe a way to make sure I receive them in their desired run?
Your code is correct and as i see only one reason can be cause such behaviour. It is android system, for avoid drain battery, decreases frequency of accelerometer stream of events in some time after app go to background or device fall asleep.
You need to revise all axelerometer related logic and optimize according
Doze and App Standby
Also you can try to work with axelerometer in foreground service.
in my ionic/angularjs application I'm using the geolocation plugin: https://github.com/apache/cordova-plugin-geolocation
Like in the documentation I use this to configure the watch:
var watchOptions = {
frequency : 10*1000,
timeout : 60*60*1000,
enableHighAccuracy: true // may cause errors if true
};
watch = navigator.geolocation.watchPosition(on_success,on_error,watchOptions);
But however on android the frequency is much higher than 10 seconds (about 0.5 Seconds). On iOS it works great. What is the problem here?
Updated in light of comments below
There is no frequency parameter available in the geolocation options for watchPosition() hence any value you pass will be ignored. The success callback registered via watchPosition() is invoked each time the native location manager receives a position update from the GPS hardware (in the case of enableHighAccuracy=true) so it's not called on a fixed interval.
The native location managers (both Android and iOS) are event-driven, i.e. they receive updates from the GPS hardware as and when it delivers them at a non-fixed interval. Hence trying to apply a fixed frequency to this is trying to fit a square peg in a round hole - you cannot demand that the GPS hardware deliver you a location update exactly ever N seconds.
While you can call getCurrentPosition() on a fixed interval, this method simply returns last received position or requests a new one.
If the problem is that the updates are too frequent, you could log the time each update is received at, and only accept the next update after N seconds, e.g.
var lastUpdateTime,
minFrequency = 10*1000,
watchOptions = {
timeout : 60*60*1000,
maxAge: 0,
enableHighAccuracy: true
};
function on_success(position){
var now = new Date();
if(lastUpdateTime && now.getTime() - lastUpdateTime.getTime() < minFrequency){
console.log("Ignoring position update");
return;
}
lastUpdateTime = now;
// do something with position
}
navigator.geolocation.watchPosition(on_success,on_error,watchOptions);
This, however, will not stop the device requesting updates more frequently, hence consuming a relatively large amount of battery.
The native Android LocationManager does allow you to specify a minimum time between updates when requesting location in order to minimise battery drain, however cordova-plugin-geolocation on Android doesn't implement use LocationManager directly, but instead uses the W3C Geolocation API Specification in the native webview, which does not allow you to specify this.
However, you can use this plugin to do this: cordova-plugin-locationservices
It will allow you to specify:
interval: Set the desired interval for active location updates, in milliseconds.
fastInterval: Explicitly set the fastest interval for location updates, in milliseconds.
I need to ensure the location I use is fresh:
Is there a way of finding out how old the location result returned by LocationServices.FusedLocationApi.getLastLocation is?
If not: if I register a location listener to the LocationServices.FusedLocationApi.requestLocationUpdates (with setNumUpdates(1) and setMaxWaitTime(0)) then will it update if the location has not changed from the one returned by LocationServices.FusedLocationApi.getLastLocation?
Yes, there is a very easy way. You can get the time of a Location fix by calling getTime() like this:
Location currentLocation = LocationServices.FusedLocationApi.getLastLocation(apiClient);
long locationAge = System.currentTimeMillis() - currentLocation.getTime();
if (locationAge <= 60 * 1000) { // not older than 60 seconds
// do something with the location
}
The documentation recommends not to use System.currentTimeMillis() for time comparisons, but I never experienced any flaws with this method. However, you should consider reading the (short) documentation:
https://developer.android.com/reference/android/location/Location.html#getTime()
To expand on Illiminat's answer, as of API 17 the getElapsedRealtimeNanos() method has been added. From the documentation of the method...
Return the time of this fix, in elapsed real-time since system boot.
This value can be reliably compared to SystemClock.elapsedRealtimeNanos(), to calculate the age of a fix and to compare Location fixes. This is reliable because elapsed real-time is guaranteed monotonic for each system boot and continues to increment even when the system is in deep sleep (unlike getTime().
https://developer.android.com/reference/android/location/Location.html#getElapsedRealtimeNanos()
Therefore the following should now be the most precise way of doing calculating this
Location currentLocation = LocationServices.FusedLocationApi.getLastLocation(apiClient);
long locationAge = SystemClock.elapsedRealtimeNanos() - currentLocation.getElapsedRealtimeNanos();
long ageLimitNanoSec = 60_000_000_000; // 60 seconds in nano seconds
if (locationAge <= ageLimitNanoSec) {
// do something with the location
}
The problem I'm having is that my count is totally off, from any pedometers, fitbit, or the Samsung Step Counter.
It appears to shut down, and not add any steps after awhile.
If I enter that I'm starting at 3000 for example, it calculates an offset and it stores as a shared preference. It tries to remain registered for the the Sensor. I also store the current steps, so that if the activity that is listening for Step Broadcasts is resumed, it will request for the steps to be output.
I have tried making the service provide notifications and be a foreground service, but the accuracy does not improve, and it uses a ton of power, I have tried a wakelock, with similiar results, not accurate, and uses too much power.
As it stands, my app does not show up in the power usage statistics, so it is hardly using power at all. The hardware sensor should be capturing steps from the accelerometers, and when it does wake up, it should output the newest steps. When it does update, it is closer to the value that I set as the step count.
I am using the Step Counter which was made available in kitkat or above, on some devices. The following code registers for the sensor.
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
if (null != sensor)
{
sensorManager.registerListener(this, sensor,
SensorManager.SENSOR_DELAY_NORMAL);
}
This is within the OnCreate Method of a Service, that I have constructed.
The Service is created as sticky. And uses a broadcast receiver to receive starting steps from an activity, to compute an offset. It also broadcasts the steps that have happened. Here is more of the code.
class MyBroadCastReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION_REQUEST_STEPS))
{
SendStepBroadcast(currentSteps);
}
else if (intent.getAction().equals(ACTION_SET_STEPS))
{
setSteps = intent.getFloatExtra(
STEPS_OCCURRED, -1);
SendStepBroadcast(setSteps);
}
}
}
#Override
public void onSensorChanged(SensorEvent event) {
if (setSteps > -1) {
offset = setSteps - event.values[0] + 1;
SharedPreferences prefs = getSharedPreferences("com.halfwaythere",
MODE_PRIVATE);
prefs.edit().putFloat("offset", offset).apply();
setSteps = -1;
}
currentSteps = event.values[0] + offset;
SharedPreferences prefs = getSharedPreferences("com.halfwaythere",
MODE_PRIVATE);
prefs.edit().putFloat("currentSteps", currentSteps).apply();
SendStepBroadcast(currentSteps);
}
private void SendStepBroadcast(float steps) {
Intent broadcastSteps = new Intent();
broadcastSteps.setAction(ACTION_STEPS_OCCURRED);
broadcastSteps.putExtra(STEPS_OCCURRED, steps);
this.sendBroadcast(broadcastSteps);
}
In the Activity the following code is used to start the service:
#Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, StepService.class);
startService(intent);
}
My most recent set of attempts to fix this, I tried the following:
Use android:process=":background" to start the service in it's own process. I will try this tomorrow in a field test, and see how it works.
I tried setting the count to zero, and found out my code would not allow, so in the above code I used > -1. Since -1 shouldn't be a valid step count.
All of the code above has test wrapped around it, and I've tried to find any edge cases, and have looked over stack overflow for pedometer problems with Step Counter on Samsung Galaxy S4. Or anything about best practices.
Thanks, and if you need any additional info, please let me know.
From my research:
When the screen is locked, the Hardware Sensor waits before outputting steps. It does count steps, but once you press the button on the side it wakes up, and it receives a SensorEvent.
Service was occasionally having Start Command being called. Was very important that I only register once, so I added a boolean that would be flipped once the initialization was called, and flip off after that. Service was paused, but not killed, and Start Command would run upon it being awoken.
Setting Service to Foreground, seems to be getting closer to the value from other pedometers. Uses more battery, but is keeping it going more, so that makes sense.
Did not require wakelock, it was only on for several milliseconds, but that did not cause the hardware sensor to send results.
Will have my Service run till goal is achieved, or offer the option to stop tracking at half way point. I really just wanted something to tell me that I would get my goal if I turned around, anything else will be equivalent to extra steps.
So I will apply what I found and continue on with my app, should be on the App Store by mid July.
I developed a Data collector which collects data from Accelerometer, Gyroscope, Magnetometer and it worked fine for a while. Then I added Linear Acceleration to it as well (After 4 months, this week). Now both the version are behaving very strangely. Sometime they log the data perfectly when I do some physical activities like walking etc. However, sometimes it doesn't update sensors values and just repeat old values i.e each sensor value is updated lets after 5 seconds, 2 sec etc randomly and I need a sampling rate of 50 samples per second. I experimented with 10-15 participants and all my data was invalid because of this. The strange things is that the same app has worked perfectly before. I can't find any problem in it. I am placing some of the snapshots here. May be if someone can point to any bug or something ?
The buffered Writter:
FileWriter fow;
BufferedWriter bow;
extfile = new File(extfilepath, message + ".csv");
fow = new FileWriter(extfile);
bow = new BufferedWriter(fow);
This bow.writer is then being used in timertask thread to log data every 20 milliseconds.
Can anyone please comment or help me with this ? This weird behavior of this app is beyond my understanding.
Check that you have a wake lock acquired if your application goes to background. I've used PowerManager.PARTIAL_WAKE_LOCK successfully in a data collection application.
When your display turns off, your application is at least paused (and system might even stop it). The partial wake lock "Ensures that the CPU is running; the screen and keyboard backlight will be allowed to go off." So reading between the lines it means that otherwise your CPU might go to sleep for small periods of time in order to save power.
Did you forget to paste in:
else if (event.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION){} ?
Are you using the accelerometer data, then subtracting gravity?
OK. What's your code look like to call the timer?? Something like this?
Timer updateTimer = new Timer("linear accel");
updateTimer.scheduleAtFixedRate(new TimerTask() {
public void run() {
updateGUI();
}
}, 0, 100);
}
private void updateGUI() {
runOnUiThread(new Runnable() {
public void run() {} } ?