How to send LocationResult with last location to PendingIntent - android

How can I send a PendingIntent with a LocationResult?
I wanted to use same PendingIntent I setup for receiving location changes to also receive the last location.
See the last lines in the code:
// Create pending intent for a service which received location changes
Intent locationIntent = new Intent(getApplicationContext(), LocationIntentService.class);
PendingIntent pendingIntent = PendingIntent.getService(
getApplicationContext(),
0,
locationIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
// Start listening to location changes
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, pendingIntent);
// Get last location
Location lastLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
// Send last location
if(lastLocation != null) {
// How to send it to the pending intent?
// Result result = LocationResult.create( ... )
// pendingIntent.send(result)
}

Inspecting the code for LocationResult.extractResult(intent) it is possible to see that LocationResult is nothing more than a Parcelable extra inserted in the Intent with a specific Id. The following Kotlin function inserts a List<Location> in a given Intent as a LocationResult
private const val EXTRA_LOCATION_RESULT = "com.google.android.gms.location.EXTRA_LOCATION_RESULT"
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun insertResult(intent: Intent, locations: List<Location>) {
val result = LocationResult.create(locations)
intent.putExtra(EXTRA_LOCATION_RESULT, result)
}
Note: FusedLocationApi has been deprecated and FusedLocationProviderClient should be used instead. I have applied the solution above to unit test the BroadcastReceiver receiving updates from FusedLocationProviderClient

Related

Open my app automatically when in Range of certain location

I am using an intent to switch to Google Maps to get the route to a marker that's shown on a map that's implemented in my app. My question is if there is any way to let my app in background to send the location at certain intervals of time, so when the user gets close to the location, when the app is reopened to have the location updated directly, so I would be able to implement the next step of the app.( the app should do something when he is close)
You can try create and monitor geofence for detect user gets close to the location:
public class MainActivity extends AppCompatActivity {
// ...
private PendingIntent getGeofencePendingIntent() {
// Reuse the PendingIntent if we already have it.
if (geofencePendingIntent != null) {
return geofencePendingIntent;
}
Intent intent = new Intent(this, GeofenceBroadcastReceiver.class);
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when
// calling addGeofences() and removeGeofences().
geofencePendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.
FLAG_UPDATE_CURRENT);
return geofencePendingIntent;
}
and then, when user gets close to the location start tracking it directly from your app. Something like that:
public class GeofenceBroadcastReceiver extends BroadcastReceiver {
// ...
protected void onReceive(Context context, Intent intent) {
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
if (geofencingEvent.hasError()) {
String errorMessage = GeofenceStatusCodes.getErrorString(geofencingEvent.getErrorCode());
Log.e(TAG, errorMessage);
return;
}
// Get the transition type.
int geofenceTransition = geofencingEvent.getGeofenceTransition();
// Test that the reported transition was of interest.
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
// Get the geofences that were triggered. A single event can trigger
// multiple geofences.
List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
// Get the transition details as a String.
String geofenceTransitionDetails = getGeofenceTransitionDetails(
this,
geofenceTransition,
triggeringGeofences
);
// Start tracking user position directly here
...
} else if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
// Stop tracking user position directly here
...
else {
// Log the error.
Log.e(TAG, getString(R.string.geofence_transition_invalid_type,
geofenceTransition));
}
}
}
I'm not sure it will work or not but you can try this one. my guess should work.
ActivityManager activityManager = (ActivityManager) getApplicationContext()
.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.moveTaskToFront(getTaskId(), 0);
Please note that you cannot keep to get the user location in the background when the system is greater than or equal to android O
Background Execution Limits
Solution : You can register a Service in the foreground.
Step 1:Create your own service
Step 2:register with Androidmanifest
Step 3:Create BroadcastReceiver receiver data
Step 4:In onResume bind and onstop unbind service
foreground example from google

Why android FusedLocationApi broadcasting same location within a period of few milliseconds?

I have an android app in which I have used FusedLocationApi to get location updates of the user.
Following is the scenario:
1. I have a singleton Watcher class in which I define the pending intent to get the locations even when app is in background. Following is the code:
private PendingIntent pendingIntent;
private Watcher() {
Intent locationIntent = new Intent(context, Receiver.class);
pendingIntent = PendingIntent.getBroadcast(
context, 007 /*requestcode*/, locationIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
Then, when I have successfully connected location services, I request for location updates:
LocationServices.FusedLocationApi.
requestLocationUpdates(googleApiClient, locationRequest, pendingIntent);
Now, whenever Receiver class which extends WakefulBroadcastReceiver gets the location update, it starts the Service.class. The service class extends IntentService.
Following is the code:
#Override
synchronized protected void onHandleIntent(final Intent intent) {
if(LocationResult.hasResult(intent)) {
LocationResult locationResult= LocationResult.extractResult(intent);
Location location = locationResult.getLastLocation();
printLocation(location);
}
So my question is, given the above steps, why does the onHandleIntent gets woken up by the LocationReceiver multiple times within a period of 5 milliseconds. The lat, lng and accuracy are all the same. I have defined the
setFastestInterval(5 seconds); and
setInterval(1 minute);
Also the location accuracy is BALANCED_POWER_ACCURACY;
In my app, the
LocationServices.FusedLocationApi.
requestLocationUpdates(googleApiClient, locationRequest, pendingIntent);
do gets called multiple times. But according to the documentation: "Any previously registered requests that have the same PendingIntent (as defined by equals(Object)) will be replaced by this request." And I am using the same pendingIntent object to call requestLocationUpdates.
Thanks in advance.
Hi Kanika you can set the minimum displacement before passing your location request object this will help you to get updates only when you have certain minimum displacement. you can use below method for doing same.
setSmallestDisplacement(float smallestDisplacementMeters)
Also FYI api reference link , https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest

Android FusedLocationProviderApi: Incoming intent has no LocationResult or LocationAvailability

I am trying to subscribe to location updates via Google's FusedLocationProviderApi. I want to receive updates in the background, so that I will receive updates even if the app is killed. Following the online documentation as best as I can, I've written the following code. Note: this is being done in an intent service, not on the UI thread, which is why I'm using blocking connect/result methods.
private void startLocationServices(String deviceId, int pollingInterval) {
Log.i(TAG, "Starting location services with interval: " + pollingInterval + "ms");
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
final PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wakeLock.acquire();
final GoogleApiClient googleApiClient =
new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.build();
ConnectionResult result = googleApiClient.blockingConnect();
if (!result.isSuccess() || !googleApiClient.isConnected()) {
Log.e(TAG, "Failed to connect to Google Api");
wakeLock.release();
return;
}
LocationRequest locationRequest = new LocationRequest();
locationRequest.setInterval(pollingInterval);
locationRequest.setFastestInterval(10000);
locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
Intent locationIntent = new Intent(this, GeoBroadcastReceiver.class);
locationIntent.putExtra(EXTRA_LOCATION_UPDATE_DEVICE_ID, deviceId);
locationIntent.setAction(GeoBroadcastReceiver.ACTION_LOCATION_UPDATE);
PendingIntent locationPendingIntent = PendingIntent.getBroadcast(
this, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingResult pendingResult = LocationServices.FusedLocationApi
.requestLocationUpdates(googleApiClient, locationRequest, locationPendingIntent);
Result requestResult = pendingResult.await();
Status requestStatus = requestResult.getStatus();
if (requestStatus.isSuccess()) {
Log.i(TAG, "Successfully subscribed to location updates.");
} else {
Log.e(TAG, String.format(
"Failed subscribe to location updates. Error code: %d, Message: %s.",
requestStatus.getStatusCode(),
requestStatus.getStatusMessage()));
}
googleApiClient.disconnect();
wakeLock.release();
}
When I run this, I see that requestStatus.isSuccess() returns true, indicating that I've successfully subscribed to the location updates. Additionally, The GeoBroadcastReciever, which extends WakefulBroadcastReceiver, receives an intent at the correct polling interval, with the correct action. Good so far, it would seem. Here is what I'm doing in the onReceive method for the GeoBroadcastReceiver:
if (LocationResult.hasResult(intent)) {
LocationResult locationResult = LocationResult.extractResult(intent);
Location location = locationResult.getLastLocation();
if (location != null) {
GeoMonitoringService.wakefulLocationUpdate(context, location);
} else {
Log.e(TAG, "LocationResult does not contain a LastLocation.");
}
} else {
Log.e(TAG, "Intent does not contain a LocationResult.");
}
The problem is, whenever the intent comes in, it does not contain the LocationResult, nor does it contain the LocationAvailabilityResult. I inspected the incoming intent in the debugger, and the only item in the intent's extras is the extra I added when setting up the intent (the device id). As such, LocationResult.hasResult() returns false. Every single time.
I've tried this on a Galaxy Note 4 running 4.0.1, and a Nexus 4 running 5.1.1, with the same result.
If I disable location on the phone, I stop receiving intents altogether, as expected.
Remove the extras from the pending intent, otherwise the location result is not delivered. I can't find where in the documentation this is explained but I found out after lot of trial and error.
A workaround (Christophe Beyls suggested that only Intent Data should be used)
So, since I only need to send a few parameters, so I do something like this:
while building the Intent before the requestLocationUpdates:
intent.setData(Uri.parse("http://a.com/a?"+ Param1+ "?" + Param2+ "?" + Param3);
and in the BroadcastReceiver:
String[] parameters = intent.getDataString().split("[?]");
This works fine, and intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED)
does return the location.
You can use:
int id = 7;
String name = "myName";
uriBuilder.scheme("http")
.authority("workaround.com")
.appendPath("extra")
.appendQueryParameter("id", String.valueOf(id))
.appendQueryParameter("name", name);
intent.setData(uriBuilder.build());
and
#Override
protected void onHandleIntent(Intent intent) {
if (LocationResult.hasResult(intent)) {
int id = Integer.valueOf(uri.getQueryParameter("id"));
String name = uri.getQueryParameter("name");
....
}
}

Remove Geofence after triggered

I use Geofences in my app, everything works fine except the removal of triggered geofences.
I red the guide from the official documentation of Android but they don't explain how to remove a geofence inside of the IntentService.
Here is the code of the event handler of the service:
#Override
protected void onHandleIntent(Intent intent)
{
Log.e("GeofenceIntentService", "Location handled");
if (LocationClient.hasError(intent))
{
int errorCode = LocationClient.getErrorCode(intent);
Log.e("GeofenceIntentService", "Location Services error: " + Integer.toString(errorCode));
}
else
{
int transitionType = LocationClient.getGeofenceTransition(intent);
if (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER)
{
List <Geofence> triggerList = LocationClient.getTriggeringGeofences(intent);
String[] triggerIds = new String[triggerList.size()];
for (int i = 0; i < triggerIds.length; i++)
{
// Store the Id of each geofence
triggerIds[i] = triggerList.get(i).getRequestId();
Picture p = PicturesManager.getById(triggerIds[i], getApplicationContext());
/* ... do a lot of work here ... */
}
}
else
Log.e("ReceiveTransitionsIntentService", "Geofence transition error: " + Integer.toString(transitionType));
}
}
How can I delete the geofence after he got triggered ?
You can do something like this:
LocationServices.GeofencingApi.removeGeofences(
mGoogleApiClient,
// This is the same pending intent that was used in addGeofences().
getGeofencePendingIntent()
).setResultCallback(this); // Result processed in onResult().
And your getGeofencePendingIntent() method can look like this:
/**
* Gets a PendingIntent to send with the request to add or remove Geofences. Location Services
* issues the Intent inside this PendingIntent whenever a geofence transition occurs for the
* current list of geofences.
*
* #return A PendingIntent for the IntentService that handles geofence transitions.
*/
private PendingIntent getGeofencePendingIntent() {
Log.d(TAG, "getGeofencePendingIntent()");
// Reuse the PendingIntent if we already have it.
if (mGeofencePendingIntent != null) {
return mGeofencePendingIntent;
}
Intent intent = new Intent(mContext, GeofenceIntentServiceStub.class);
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
// addGeofences() and removeGeofences().
mGeofencePendingIntent = PendingIntent.getService(mContext, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
return mGeofencePendingIntent;
}
You would proceed as you did when adding Geofences (create a LocationClient and wait for it to connect). Once it is connected, in the onConnected callback method, you would call removeGeofences on the LocationClient instance instead and pass it a list of request IDs you want to remove and an instance of OnRemoveGeofencesResultListener as a callback handler.
Of course, you must use the same request IDs you used when creating the GeoFence with GeoFence.Builder's setRequestId.
#Override
public void onConnected(Bundle arg0) {
locationClient.removeGeofences(requestIDsList,
new OnRemoveGeofencesResultListener() {
...
});

requestLocationUpdates with PendingIntent and Broadcast - what broadcasts do I get

I have setup an alarm which is received by a BroadcastReceiver which launches a WakefulIntentService (class LocationMonitor). In the LocationMonitor I have :
private static final int MIN_TIME_BETWEEN_SCANS = 1 * 30 * 1000;
private static final int MIN_DISTANCE = 0;
#Override
protected void doWakefulWork(Intent intent) {
final CharSequence action = intent.getAction();
if (action == null) { // monitor command from the alarm manager
// the call below enables the LocationReceiver
BaseReceiver.enable(this, ENABLE, LocationReceiver.class);
if (lm == null) lm = (LocationManager) this
.getSystemService(Context.LOCATION_SERVICE);
Intent i = new Intent(this, LocationReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(this, NOT_USED, i,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT);
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER,
MIN_TIME_BETWEEN_SCANS, MIN_DISTANCE, pi);
} else if (ac_location_data.equals(action)) {
final Bundle extras = intent.getExtras();
if (extras != null) {
final Location loc = (Location) extras
.get(LocationManager.KEY_LOCATION_CHANGED);
if (loc == null) {
w("NULL LOCATION - EXTRAS : " + extras); //Log.w
// while gps is disabled I keep getting this :
// NULL LOCATION - EXTRAS : Bundle[{providerEnabled=false}]
} else {
final double lon = loc.getLongitude();
final double lat = loc.getLatitude();
w("latitude :" + lat + " -- longitude : " + lon);
}
}
}
}
I have several issues with the code above.
If GPS is initially disabled and then I enable it I get a bunch of W/GpsLocationProvider(...): Unneeded remove listener for uid 1000. The warning comes from here. I can't find in the code where is this removing of listeners triggered, nor can I see where they are assigned the uid 1000 (apparently the system server).
When I enable the gps I get the location as expected and then a "RemoteException"
LocationManagerService(...): RemoteException calling onLocationChanged on Receiver{4083ee68 Intent PendingIntent{4084e6b8: PendingIntentRecord{4083ef78 gr.uoa.di.monitoring.android broadcastIntent}}}mUpdateRecords: {gps=UpdateRecord{40838180 mProvider: gps mUid: 10064}}
which is not really a RemoteException, just a PendingIntent.CancelledException - the message is quite misleading. Or so I think : it comes from here which calls this. My question is : why is it reusing the Intent - shouldn't the FLAG_ONE_SHOT dispose of it ?
But the most important question is : when I register a PendingIntent like this what intents do I expect to receive ? And what flags should I use ?
Keep in mind I am using this pattern cause I want to have the phone update its position even when asleep and this achieves it (I do get the location updates). I try to simulate requestSingleUpdate (unavailable in 2.3) using FLAG_ONE_SHOT.
Receiver :
public final class LocationReceiver extends BaseReceiver {
private static final Class<? extends Monitor> MONITOR_CLASS =
LocationMonitor.class;
#Override
public void onReceive(Context context, Intent intent) {
d(intent.toString());
final String action = intent.getAction();
d(action + "");
final Intent i = new Intent(context, MONITOR_CLASS);
i.fillIn(intent, 0); // TODO do I need flags ?
i.setAction(ac_location_data.toString());
WakefulIntentService.sendWakefulWork(context, i);
}
}
To this question:
when I register a PendingIntent like this what intents do I expect to
receive ? And what flags should I use ?
When you register for location updates and pass a PendingIntent, this PendingIntent will be triggered when the LocationManager decides to inform you about a location update. You can provide pretty much whatever you want, depending on what you want to happen when the PendingIntent is triggered. The LocationManager will add an extra to the Intent that is sent. This extra has the bundle key LocationManager.KEY_LOCATION_CHANGED and the object associated with that key is a Location object.
LocationManager will use this PendingIntent again and again to inform your app of location updates, so I think using PendingIntent.FLAG_ONE_SHOT is probably not such a good idea. If you only want a single update, why don't you just unregister after you get one update?
EDIT: Add code to cancel any previously requested updates before registering for updates
Before you call registerLocationUpdates(), do this to cancel any previously registered updates:
Intent i = new Intent(this, LocationReceiver.class);
// Get any existing matching PendingIntent
PendingIntent pi = PendingIntent.getBroadcast(this, NOT_USED, i,
PendingIntent.FLAG_NO_CREATE);
if (pi != null) {
// Cancel any updates for this PendingIntent, because we are about to
// invalidate it
lm.removeUpdates(pi);
}
// Create a new PendingIntent and cancel any previous one
pi = PendingIntent.getBroadcast(this, NOT_USED, i,
PendingIntent.FLAG_CANCEL_CURRENT);
// Now register for location updates...
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER,
MIN_TIME_BETWEEN_SCANS, MIN_DISTANCE, pi);
NOTE: Actually, I don't know why you need to cancel any previous PendingIntent and create a new one in this case. You can just get a PendingIntent and if you have already registered for location updates with that PendingIntent, I don't think that registering again will cause the PendingIntent to be used multiple times. If you want to try that, all you need to do is to remove PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT from your existing code. I think that is a better/cleaner/clearer solution.

Categories

Resources