I am making an app that will draw a user's path on a map. This is all working fine. However, I also want to continue to collect the path (when requested by the user) when the app is in the background. This also is working pretty good, but occasionally I get "glitches". See this map for an example:
GoogleMap
I believe what is happening is that when I start the service, I am getting the "last known location" instead of the current location. I have tried several techniques without much luck so far. Some other ideas that I have have had:
Passing the GoogleApiClient from the Activity to the Service, already connected (is this possible?)
Ignoring the first "X" location updates in the service (how big does "X" need to be?)
Cleansing the path, to remove "bad" locations (what kind of algorithm would I need to figure out what a "bad" location is...it is easy to see in the attached map, but I am not sure how to do it in code).
Any other suggestions on the best way to handle this scenario?
Here is some code to give you an idea of the current functionality:
Activity.java:
#Override
protected void onCreate(Bundle savedInstanceState) {
...
setUpMapIfNeeded();
buildGoogleApiClient();
createLocationRequest();
...
}
private void buildGoogleApiClient() {
googleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(this).addOnConnectionFailedListener(this).addApi(LocationServices.API).build();
}
private void createLocationRequest() {
locationRequest = new LocationRequest();
locationRequest.setInterval(Utils.LOCATION_UPDATE_MS);
locationRequest.setFastestInterval(Utils.FASTEST_LOCATION_UPDATE_MS);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}
#Override
public void onConnected(Bundle bundle) {
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this);
location = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
Log.v("Connected", String.format("Location: %f, %f, %f", location.getLatitude(), location.getLongitude(), location.getAccuracy()));
if (location != null) {
updateMap();
}
}
#Override
protected void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(this).unregisterReceiver(pathReceiver);
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, this);
if (isNavigatingTo()) {
Preferences.save(Preferences.PREFERENCE_NAVIGATE_TO, navigateToWaypoint.getId());
} else {
Preferences.remove(Preferences.PREFERENCE_NAVIGATE_TO);
}
if(isRecordingTrack()){
// Start the PathSevice to continue recording the track.
Intent pathServiceIntent = new Intent(this, PathService.class);
pathServiceIntent.putExtra(Utils.PATH_EXTRA, path);
startService(pathServiceIntent);
}
}
...
PathService.java:
#Override
public void onCreate() {
buildGoogleApiClient();
createLocationRequest();
googleApiClient.connect();
super.onCreate();
}
private void buildGoogleApiClient() {
googleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(this).addOnConnectionFailedListener(this).addApi(LocationServices.API).build();
}
private void createLocationRequest() {
locationRequest = new LocationRequest();
locationRequest.setInterval(Utils.LOCATION_UPDATE_MS);
locationRequest.setFastestInterval(Utils.FASTEST_LOCATION_UPDATE_MS);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}
#Override
public void onConnected(Bundle bundle) {
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this);
}
#Override
public void onLocationChanged(Location location) {
path.addPoint(location.getLatitude(), location.getLongitude());
}
So the solution was in my question...as it seemed I was adding the "last known location" when returning back to the activity from the service (see onConnected in the Activity class above).
I now only start the location requests in the onConnected.
Related
I am new to android development and was working on an app which requires google map activity.
Problem I am facing is that when I am trying to pan(or scroll) through the map I am immediately respawned to my current location which I set initially.
A little help would be wonderful as I am stuck at this point and unable to find a solution.
Here is the code:-
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMapsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
}
#Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
mMap.getUiSettings().setScrollGesturesEnabled(true);
locationManager=(LocationManager)this.getSystemService(Context.LOCATION_SERVICE);
locationListener=new LocationListener() {
#Override
public void onLocationChanged(#NonNull Location location) {
centerOnMap(location,"Your Location");
}
};
if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)!= PackageManager.PERMISSION_GRANTED)
{
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.ACCESS_FINE_LOCATION},1);
}
else{
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,0,0,locationListener);
Location lastKnownLocation=locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
centerOnMap(lastKnownLocation,"Your Location");
}
}
public void centerOnMap(Location location,String address)
{
LatLng userLocation = new LatLng(location.getLatitude(),location.getLongitude());
mMap.addMarker(new MarkerOptions().position(userLocation).title(address));
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(userLocation, 15));
}
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull #org.jetbrains.annotations.NotNull String[] permissions, #NonNull #org.jetbrains.annotations.NotNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED)
{
if(ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION)==PackageManager.PERMISSION_GRANTED){
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,0,0,locationListener);
}
}
}
}
The one requirement that you likely have but not stated is:
When the lastLocation becomes available and the user has
not moved the map then center the map on the location. If the
user has already moved the map then do not center the map. In
either case, add a marker at the user's location.
Before getting too far along it must be noted that Google Maps provides a
feature similar to what you are trying to achieve, though you'd still have to "move camera". The marker is a blue-ball and not the typical marker. See myMap.setMyLocationEnabled(true). That's it! Do this when you have gotten map permissions.
But in the event you don't want to use that then here are the simple changes you need.
Remember that the LocationManager getLastKnownLocation can return
null if the device does not have one (yet). So I'd recommend one small
change a bit unrelated - just let the location listener do all the work and get rid of this one special case:
// this is where you initially check permissions and have them.
else{
locationManager.requestLocationUpdates (LocationManager.GPS_PROVIDER,0,0,locationListener);
// Here I removed the last location centering and let the
// location listener always handle it.
}
So this opens the possibility
that the user could be interacting with map and finally the last location arrives. I understand this to be the issue you are trying to address.
(As an aside, it seems to me you are mixing the use of the android.location.LocationManager with
the FusedLocationProviderApi (com.google.android.gms.location) so I couldn't get your
code to compile due to incompatible LocationListeners.
Unfortunately, Google Maps has two LocationListener classes so
to be certain you'd have to include your imports to understand further.)
Anyways...
When the map is first ready (onMapReady) the camera of the map is
centered at (0,0). You can get the cameras target position (center)
at any time using LatLng tgtCtr = mMap.getCameraPosition().target;.
Oddly enough it is not straightforward to know whether the user has
interacted with the map in any way: scrolling events generate camera
changes while touch events generate a separate event. Camera changes
cannot be used exclusively because your code or the user could just
zoom which is not moving the map. You could go this route but
for the purposes of this answer, to keep things simple, the camera
target is used.
Declare a class instance variable (same area where you define mMap):
LatLng tgtCtr;
So in your onMapReady after assigning mMap do:
tgtCtr = mMap.getCameraPosition().target;
So assume your code exists as you posted (it's pretty close) then these
changes may help:
// This change simply restricts centering of the map on location
// update to only when user has not moved the map (scrolled).
#Override
public void onLocationChanged(#NonNull Location location) {
LatLng currentCtr = mMap.getCamaraPosition().target;
// This is not the ideal check since `double` comparisons
// should account for epsilon but in this case of (0,0) it should work.
// Alternatively you could compute the distance of current
// center to (0,0) and then use an epsilon:
// see `com.google.maps.android.SphericalUtil.computeDistanceBetween`.
if (currentCtr.latitude == 0 && currentCtr.longitude == 0) {
centerOnMap(location,"Your Location");
}
}
It also seems like a good idea to save the marker which was added for user's
location - this is optional but may come in handy to prevent multiple markers
from being added at that one location:
// Define a class instance variable
Marker myLocMarker = nulll;
// and then in centerOnMap
public void centerOnMap(Location location, String address)
{
// ... other code
if (myLocMarker == null) {
myLocMarker = mMap.addMarker(new MarkerOptions().position(userLocation).title(address));
}
// ... more code
}
So really the only difficulty in this is figuring out "has the
user moved the map." In this case, based on the initial requirement
you would not want to move the map.
as you mention in comment section use FusedLocationProviderClient instead of LocationManager.
add implementation 'com.google.android.gms:play-services-location:17.0.0' in app level gradle. and don't forget to add manifest permission for fine location.
public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {
private GoogleMap mMap;
FusedLocationProviderClient mFusedLocationClient;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_maps);
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
}
#Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
mMap.getUiSettings().setScrollGesturesEnabled(true);
getLastLocation();
mMap.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() {
#Override
public void onMapLongClick(#NonNull LatLng latLng) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latLng.latitude);
location.setLongitude(latLng.longitude);
centerOnMap(location,"Your location");
}
});
}
#SuppressLint("MissingPermission")
private void getLastLocation() {
if (checkPermissions()) {
if (isLocationEnabled()) {
mFusedLocationClient.getLastLocation().addOnCompleteListener(new OnCompleteListener<Location>() {
#Override
public void onComplete(#NonNull Task<Location> task) {
Location location = task.getResult();
if (location == null) {
requestNewLocationData();
} else {
centerOnMap(location,"Your Location");
}
}
});
} else {
Toast.makeText(this, "Please turn on" + " your location...", Toast.LENGTH_LONG).show();
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
}
} else {
requestPermissions();
}
}
#SuppressLint("MissingPermission")
private void requestNewLocationData() {
LocationRequest mLocationRequest = new LocationRequest();
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
mLocationRequest.setInterval(5);
mLocationRequest.setFastestInterval(0);
mLocationRequest.setNumUpdates(1);
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
mFusedLocationClient.requestLocationUpdates(mLocationRequest, mLocationCallback, Looper.myLooper());
}
private LocationCallback mLocationCallback = new LocationCallback() {
#Override
public void onLocationResult(LocationResult locationResult) {
Location mLastLocation = locationResult.getLastLocation();
centerOnMap(mLastLocation,"Your Location");
}
};
private boolean checkPermissions() {
return ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
private void requestPermissions() {
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.ACCESS_FINE_LOCATION}, 1);
}
private boolean isLocationEnabled() {
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
public void centerOnMap(Location location,String address)
{
LatLng userLocation = new LatLng(location.getLatitude(),location.getLongitude());
mMap.clear();
mMap.addMarker(new MarkerOptions().position(userLocation).title(address));
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(userLocation, 15));
}
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED)
{
getLastLocation();
}
}
}
Following the examples from google developers site, I use FusedLocationProviderClient to get de last known location.
private FusedLocationProviderClient fusedLocationClient;
// ..
#Override
protected void onCreate(Bundle savedInstanceState) {
// ...
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
}
And on button click, I call:
fusedLocationClient.getLastLocation()
.addOnSuccessListener(this, new OnSuccessListener<Location>() {
#Override
public void onSuccess(Location location) {
// Got last known location. In some rare situations this can be null.
if (location != null) {
// Logic to handle location object
myposition.setText(location.getLatitude() + ", " + location.getLongitude());
// ...
}
}
});
But, sometimes, it gives me a location that is far from current device geoposition. I've checked it on three different devices. I have declared:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
How should I use fusedlocation to get the best position with accuary? I've tried to change getlastlocation with getcurrentlocation but this last method is not available, although the docs said it is available.
I am trying to listen to location changes using the Play Services Location API.
What I'm trying to do is go for the background app approach and get the updates using a PendingIntent. However, the onHandleIntent() function does not get called at all.
I didn't find a single source of comprehensive documentation regarding this approach. Can you tell me is there something I'm doing wrong?
public class LocationCollector extends IntentService implements GoogleApiClient.ConnectionCallbacks
, GoogleApiClient.OnConnectionFailedListener {
private Context mContext;
private GoogleApiClient mLocationClient;
public LocationCollector(Context context){
super("LocationCollector");
mContext = context;
mLocationClient = new GoogleApiClient.Builder(mContext)
.addApi(LocationServices.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
public void start(){
mLocationClient.connect();
}
public void stop(){
mLocationClient.disconnect();
}
#Override
public void onConnected(Bundle bundle) {
LocationRequest request = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
Intent locationIntent = new Intent(mContext, LocationCollector.class);
PendingIntent locationPendingIntent = PendingIntent.getService(mContext, 1, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingResult<Status> result = LocationServices.FusedLocationApi.requestLocationUpdates(mLocationClient, request, locationPendingIntent);
}
#Override
public void onConnectionSuspended(int i) {
Log.d("Location","Api connection suspended");
}
#Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.d("Location","Api connection failed");
}
#Override
protected void onHandleIntent(Intent intent) {
Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
if(location != null){
String time= intent.getStringExtra("time");
Toast.makeText(mContext,location.toString(),Toast.LENGTH_SHORT).show();
}
}
}
Can you tell me is there something I'm doing wrong?
First, a Service is a Context. You do not need to — or even want to — pass in some Context to it.
Second, your constructor should never be used. Replace it with a zero-argument constructor.
Third, start() and stop() are not lifecycle methods on an IntentService, and so I am not quite certain what you are expecting will call them. That, coupled with the previous problem, means nothing will really happen with this service.
Saying that you want an IntentService to handle the locations, via an onHandleIntent() method, is reasonable. However, something outside of that service is going to need to do the connecting and disconnecting, or you are going to need a substantially more complex service.
So I have implemented the new Fused Location Provider API to get a location of the user but for some reason, I cannot get any location unless the GPS is on. Not always, will users have their GPS on and I would like to not have to ask them to turn their GPS on every time the load the app.
How can I tell the API to give me a location with whatever provider it has available?
Here is my code:
public class FusedLocationService implements
LocationListener,
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
public interface OnLocationChangedListener {
public void OnLocationChanged(Location location);
}
private final String TAG = "SE8611";
private boolean mRequestingLocationUpdates = true;
private OnLocationChangedListener mCallBack;
Service locationService;
private LocationRequest locationRequest;
private GoogleApiClient googleApiClient;
private Location mCurrentLocation;
private FusedLocationProviderApi fusedLocationProviderApi = LocationServices.FusedLocationApi;
public FusedLocationService(Service locationService, final long INTERVAL, final long FASTEST_INTERVAL) {
Logger.log(TAG, "FusedLocationService called");
this.mCallBack = (OnLocationChangedListener)locationService;
locationRequest = LocationRequest.create();
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
locationRequest.setInterval(INTERVAL);
locationRequest.setFastestInterval(FASTEST_INTERVAL);
this.locationService = locationService;
googleApiClient = new GoogleApiClient.Builder(locationService)
.addApi(LocationServices.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
if (googleApiClient != null) {
googleApiClient.connect();
}
}
#Override
public void onConnected(Bundle connectionHint) {
if(mRequestingLocationUpdates) {
startLocationUpdates();
}else{
Logger.log(TAG, "Location updates are already running.");
}
}
protected void startLocationUpdates() {
this.fusedLocationProviderApi.requestLocationUpdates(
googleApiClient, locationRequest, this);
this.mRequestingLocationUpdates = false;
}
#Override
public void onLocationChanged(Location mCurrentLocation) {
Logger.log(TAG, "onLocationChanged called");
this.mCurrentLocation = mCurrentLocation;
this.mCallBack.OnLocationChanged(this.mCurrentLocation);
}
public void startLocationUpdatesAfterResume(){
if (googleApiClient.isConnected() && !mRequestingLocationUpdates) {
Logger.log(TAG, "startLocationUpdatesAfterResume called");
this.startLocationUpdates();
}
}
public void stopLocationUpdates() {
Logger.log(TAG, "stopping Location Updates");
LocationServices.FusedLocationApi.removeLocationUpdates(
googleApiClient, this);
}
public Location getLocation() {
return this.mCurrentLocation;
}
#Override
public void onConnectionSuspended(int i) {
this.mRequestingLocationUpdates = true;
}
#Override
public void onConnectionFailed(ConnectionResult connectionResult) {
this.mRequestingLocationUpdates = true;
}
}
I had the same issue like you, which is not an issue at all.
Android used to have a GPS button that let you control it directly, but they replaced it with a Location button which works different.
In order to get any type of location, you must turn it on.
Like you, I thought the Location button turns on and off the GPS only, but that's not the case.
You can control the GPS by changing the location mode:
1. High accuracy (GPS, Wi-Fi and mobile networks)
2. Power Saving (Wi-Fi and mobile networks)
3. GPS only
I think I have found the solution of the problem.
If you go to Settings -> Privacy and Safety -> Location, you would notice that Location is not only GPS, but it actually lets user decide which providers can be used. For example, you can set that only WiFi and Cellular should be used to obtain any locations
Disabling Location option will disable all providers at once, not only GPS.
To test that app for users with only WiFi can get a location – change setting to "Wifi+Cellular".
you seem to be using LocationRequest.PRIORITY_HIGH_ACCURACY, which prefers using GPS above and over other methods. You might want to use PRIORITY_BALANCED_POWER_ACCURACY, which will use WiFi and cell towers before using GPS.
Also, since PRIORITY_BALANCED_POWER_ACCURACY is a "coarse" level of accuracy, you might want to change the location permission in your manifest appropriately.
The training documentation details more information about the priority flags, you might also want to go through it.
We have a strong GPS signal I have an app for testing how fast a GPS lock can be established I even tested the navigation app on my phone they all get pretty much insta lock bellow 10 if I just turned on the GPS.
But with this app I just never get a lock, the wile loop just keeps going on and on and the Log never shows I let it run for 10min and nothing. Am I missing something here I know there is a possible > 10min delay but other apps have no problem that are not using lastKnownLocation but pulling an actual location.
Also the gpsLocation() method is in a static Locator class.
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Location location = Locator.gpsLocation(this);
while(location == null)
{
location = Locator.gpsLocation(this);
}
Log.w("GPS LOCATION", "LOCATION FOUND");
}
/**
* Finds users location using gps
* satelite network.
*
* #param FragmentActivity
* #return Location
*/
public static Location gpsLocation(FragmentActivity context)
{
// Fetch LocationManager service & instance of UserLocator
LocationManager provider = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
UserLocator locator = new UserLocator();
// Attempt to get a fix on users location
provider.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locator);
// Return users location using GPS Service
return locator.location;
}
/**
* Use to listen for user location updates
* TODO: Reimplement as a anonymous class
*/
class UserLocator implements LocationListener
{
// Public accessor
public Location location;
public void onLocationChanged(Location location)
{
if(location != null)
{
// We have a fix on users location
this.location = location;
}
}
public void onProviderDisabled(String provider) {}
public void onProviderEnabled(String provider) {}
public void onStatusChanged(String provider, int status, Bundle extras) {}
}
I think your problem is that your location updates listener is updating the location variable inside your UserLocation class, however the variable you are checking is the one you get back from the method gpsLocation, which is null.
What you are doing is you are signing up for location updates and immediately returning the value of null and checking it over and over again, while your real location is being updated in another place.
All this is also assuming that you have declares the proper permission in your manifest file.
Try this:
Location location;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
gpsLocation(this);
while(location == null)
continue;
Log.w("GPS LOCATION", "LOCATION FOUND");
}
public static void gpsLocation(FragmentActivity context)
{
LocationManager provider = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
UserLocator locator = new UserLocator();
provider.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locator);
}
class UserLocator implements LocationListener
{
public void onLocationChanged(Location loc)
{
if(loc != null)
location = loc;
}
public void onProviderDisabled(String provider) {}
public void onProviderEnabled(String provider) {}
public void onStatusChanged(String provider, int status, Bundle extras) {}
}
With this implementation, both the listener and the while loop address the same instance of Location, so once this changes from null you will get the log message.
I would however strongly discourage putting such a while(true) loop in your onCreate method, you will be blocking the main thread, and this is generally a bad thing to do.