9

I'm having trouble sending a string extra with my PendingIntent that I pass to LocationServices.FusedLocationApi.requestLocationUpdates(GoogleApiClient client, LocationRequest request, PendingIntent callbackIntent).

It appears that the username extra i'm putting onto the Intent is mangling the location that requestLocationUpdates is trying to hand off to my IntentService as intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED) returns null.

EDIT

I've tried making a User class that implements Parcelable and putting it as an extra:

mRequestLocationUpdatesIntent.putExtra("username", new User(username));

and I've also tried to put the Parcelable User inside a Bundle as suggested via comment in this bug report https://code.google.com/p/android/issues/detail?id=81812:

Bundle userBundle = new Bundle();
userBundle.putParcelable("user", new User(username));
mRequestLocationUpdatesIntent.putExtra("user", userBundle);

in my service:

Bundle userBundle = intent.getBundleExtra("user");
User user = userBundle.getParcelable("user");
String username = user.getUsername();

However neither of these approaches has made any difference. Whenever I put any extra onto my intent, the location is never added to the intent when the updates occur.

I setup this IntentService to handle location updates:

public class LocationUpdateService extends IntentService {

    private final String TAG = "LocationUpdateService";

    public LocationUpdateService() {
        super("LocationUpdateService");
    }


    @Override
    protected void onHandleIntent(Intent intent) {

        Log.d(TAG, "onHandleIntent");

        Bundle extras = intent.getExtras();
        Log.d(TAG, "keys found inside intent: " + TextUtils.join(", ", extras.keySet()));

        String username = intent.getStringExtra("username");

        if (username != null) {
            Log.d(TAG, "username: " + username);
        } else {
            Log.d(TAG, "username: null");
        }

        if (!intent.hasExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED)) {
            Log.d(TAG, "intent does not have location :(");
        }

        Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);

        if (location == null) {
            Log.d(TAG, "location == null :(");
        }

        Log.d(TAG, "latitude " + String.valueOf(location.getLatitude()));
        Log.d(TAG, "longitude " + String.valueOf(location.getLongitude()));

        ...

    }


}

When the user clicks a button, the startLocationUpdates is called in my main activity:

main activity class:

...

Boolean mLocationUpdatesEnabled = false;

protected void createLocationRequest() {
    mLocationRequest = new LocationRequest();
    mLocationRequest.setInterval(LOCATION_UPDATE_INTERVAL);
    mLocationRequest.setFastestInterval(LOCATION_UPDATE_FASTEST_INTERVAL);
    mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}

protected void startLocationUpdates() {

    Log.d(TAG, "startng location updates...");

    mLocationUpdatesEnabled = true;

    if (mLocationRequest == null) {
        createLocationRequest();
    }

    // create the Intent to use WebViewActivity to handle results
    Intent mRequestLocationUpdatesIntent = new Intent(this, LocationUpdateService.class);

    // create a PendingIntent
    mRequestLocationUpdatesPendingIntent = PendingIntent.getService(getApplicationContext(), 0,
            mRequestLocationUpdatesIntent,
            PendingIntent.FLAG_CANCEL_CURRENT);

    // request location updates
    LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient,
            mLocationRequest,
            mRequestLocationUpdatesPendingIntent);

    Log.d(TAG, "location updates started");
}

protected void stopLocationUpdates() {

    Log.d(TAG, "stopping location updates...");

    mLocationUpdatesEnabled = false;

    LocationServices.FusedLocationApi.removeLocationUpdates(
            mGoogleApiClient,
            mRequestLocationUpdatesPendingIntent);

    Log.d(TAG, "location updates stopped");
}

This all works well and good; When the user presses the button, toggleLocationUpdates is called, which calls LocationServices.FusedLocationApi.requestLocationUpdates which calls my LocationUpdateService where I'm able to get the location.

The trouble comes when I tried to put a string extra onto my Intent using Intent.putExtra(String, String):

main activity class:

...
protected void startLocationUpdates(String username) {
    ....

    // create the Intent to use WebViewActivity to handle results
    Intent mRequestLocationUpdatesIntent = new Intent(this, LocationUpdateService.class);

    //////////////////////////////////////////////////////////////////
    //
    //  When I put this extra, IntentService sees my username extra
    //  but the parcelableExtra `location` == null :(
    // 
    //////////////////////////////////////////////////////////////////

    mRequestLocationUpdatesIntent.putExtra("username", username);
    ...
}
...

EDIT I had started the next sentence as a statement rather than a question: "I am using..."

Am I using the correct approach to sending some extra data to this location update handling IntentService or is there a more-sane way to go about this?

Is this a bug or just poor documentation?

techjeffharris
  • 392
  • 2
  • 13
  • @BladeCoder has provided me with some guidance in response to my post in the Android Development Google+ community: https://plus.google.com/117366723702848823459/posts/6QAmns2pQCT I'll post my answer once I get it figured out – techjeffharris Jun 23 '15 at 02:07
  • One clarifying question, does it print out `"location == null :("` - i.e., `hasExtra()` returns `false`, or is `intent.getParcelableExtra()` just returning null? – ianhanniballake Jun 23 '15 at 04:39
  • If I remove the return statement, I get `java.lang.NullPointerException: Attempt to invoke virtual method 'double android.location.Location.getLatitude()' on a null object reference` I edited the IntentService to provide verbose logging which shows that `Intent.hasExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED` returns false, `location == null`, and the null object reference exception is thrown. – techjeffharris Jun 23 '15 at 05:04
  • 1
    I also get this log output `keys found inside intent: username`; however, when I do not put "username" on the intent, I get this output: `keys found inside intent: com.google.android.location.LOCATION, com.google.android.gms.location.EXTRA_LOCATION_RESULT` – techjeffharris Jun 23 '15 at 05:15
  • Did you ever resolve this? I'm having the same issue. – Catherine Aug 11 '15 at 23:34
  • I did not. My needs did not actually require this, so developed a workaround using the application's 'shared preferences'. – techjeffharris Aug 12 '15 at 00:20
  • Did you check whether isSuccess() is true on the PendingResult returned by requestLocationUpdates method? – user3334059 Aug 15 '15 at 14:09
  • Maybe you can try iterating over all the keys in the bundle and make sure that none of the keys that you expect exist in the bundle. – user3334059 Aug 15 '15 at 14:24
  • @SumantHanumante, yes I did both check isSuccess() is true on the PendingResult (it was) **and** I tried iterating over all the keys in the bundle and the `username` key was not there. – techjeffharris Aug 15 '15 at 17:51
  • @Catherine is my answer sufficient to help you? – andrewdleach Aug 16 '15 at 12:18
  • @andrewdleach I'm adapting it to be workable and testing it. It is missing some function implementations and is using non-API classes (such as CoordinateStorageDatabaseHelper) so it's not immediately usable. – Catherine Aug 17 '15 at 19:32
  • @Catherine would you like modification of the answer with custom classes removed? – andrewdleach Aug 17 '15 at 19:34
  • 1
    Possible duplicate of [Location Client request location updates with parcelable extras in PendingIntent](http://stackoverflow.com/questions/27303057/location-client-request-location-updates-with-parcelable-extras-in-pendingintent) – Jade Jul 13 '16 at 18:47

1 Answers1

-1

Using the IntentService coupled with the FusedLocationProviderAPI will present issues. From the Developer Docs titled Receiving Location Updates:

Depending on the form of the request, the fused location provider either invokes the LocationListener.onLocationChanged() callback method and passes it a Location object, or issues a PendingIntent that contains the location in its extended data. The accuracy and frequency of the updates are affected by the location permissions you've requested and the options you set in the location request object

Further, a PendingIntent is used for extending permissions for another piece of code (FusedLocationProviderAPI in Google Play Services) to execute their code within your apk. An IntentService is used to start a Service defined within the scope of your apk.

So, the method requires an implementation of LocationListener for foreground updates, or a PendingIntent for background updates coupled with a Broadcast Receiver.

This is a working example of some methods used to request location updates from a PendingIntent coupled with extra values.

Note: LocalStorage.java is a utility class for storing local variables, it is not part of the Android API

GPSPlotter

/**
 * Private helper method to initialize the Google Api Client with the
 * LocationServices Api and Build it for use.
 */
private void initializeGoogleApiClient() {
    mGoogleApiClient = new GoogleApiClient.Builder(mContext)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .addApi(LocationServices.API)
            .build();

}

/**
 * Private helper method to determine whether or not GooglePlayServices
 * are installed on the local system.
 *
 * @return services are installed.
 */
private boolean googlePlayServicesInstalled() {
    int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(mContext);
    return result == ConnectionResult.SUCCESS;
}

/**
 * Private method to build the Api Client for use with the LocationServices API.
 */
private synchronized void buildApiClient() {
    Log.w(TAG, "Building Google Api Client...");
    initializeGoogleApiClient();
}

/**
 * Private method used to connect the ApiClient to the Api hosted by Google for
 * Accessing Locations.
 */
private void connectClient() {
    mGoogleApiClient.connect();
}

 /**
 * User passes in a requested interval polling time in seconds as an
 * integer.
 *
 * @param theAccount is a reference to the parent activity used for updating views.
 */
public void beginManagedLocationRequests(MyAccount theAccount) {
    if (mAccount == null)
        mAccount = theAccount;

    startBackgroundUpdates();

}

/**
 * Public method to end the managed Location Requests.
 */
public void endManagedLocationRequests() {
        endBackgroundUpdates();

}

/**
 * This method handles the switch in polling rates by stopping and then starting once more the
 * background udpates, which in turn sets the interval in another method in the call stack.
 * @param theInterval the desired interval polling rate
 */
public void changeRequestIntervals(int theInterval) {
    mIntentInterval = theInterval;
    if (LocalStorage.getRequestingBackgroundStatus(mContext)) {
        endBackgroundUpdates();
        startBackgroundUpdates();
    }



}

/**
 * Private helper method to build an Intent that will be couple with a pending intent uses
 * for issuing background Location requests.
 *
 * @return theIntent
 */
private Intent buildBackgroundRequestIntent() {
    Intent intent = new Intent(mContext, BackgroundLocationReceiver.class);
    intent.setAction(BACKGROUND_ACTION);
    intent.putExtra(User.USER_ID, mUserID);
    return intent;
}

/**
 * Private helper method used to generate a PendingIntent for use when the User requests background service
 * within the FusedLocationApi until the Interval is changed.
 *
 * @return pendingIntent
 */
private PendingIntent buildRequestPendingIntent(Intent theIntent) {
    Log.w(TAG, "building pending intent");
    return PendingIntent.getBroadcast(mContext, 0, theIntent, 0);
}


/**
 * Private method to start the Location Updates using the FusedLocation API in the background.
 */
private void startBackgroundUpdates() {
    Log.w(TAG, "Starting background updates");
    if (googlePlayServicesInstalled()) {
        LocalStorage.putBackgroundRequestStatus(true, mContext);
        LocalStorage.putLocationRequestStatus(true, mContext);
        registerAlarmManager();
        LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, buildLocationRequest(), buildRequestPendingIntent(buildBackgroundRequestIntent()));
    }
}


/**
 * Private method to end background updates.
 */
private void endBackgroundUpdates() {
    Log.w(TAG, "Ending background updates");
    LocalStorage.putBackgroundRequestStatus(false, mContext);
    LocalStorage.putLocationRequestStatus(false, mContext);
    unregisterAlarmManager();
    LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, buildRequestPendingIntent(buildBackgroundRequestIntent()));
}

BackgroundLocationReceiver

public class BackgroundLocationReceiver extends BroadcastReceiver {
private static final String TAG = "BLocRec: ";
private static final String UPLOAD_ERROR_MESSAGE = "Background Service to Upload Coordinates Failed.";
private static final String UPLOAD_MESSAGE = "Coordinate Batch Pushed to Database.";

public BackgroundLocationReceiver() {
    //Default, no-arg constructor
}

/**
 * This method handles any location updates received when the app is no longer in focus. Coordinates are
 * stored in the local database and uploaded once every hour.
 * @param context the application context
 * @param intent is the pending intent
 */
@Override
public void onReceive(Context context, Intent intent) {

    if (intent.getAction().matches(GPSPlotter.BACKGROUND_ACTION)) {
        Log.w(TAG, "BLR Received-background");
        Location location = intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
        storeLocation(location, context, intent.getStringExtra(User.USER_ID));

    }

EDIT The method below builds a LocationRequest necessary for invoking the requestLocationUpdates() method

/**
 * Private helper method used to generate a LocationRequest which will be used to handle all location updates
 * within the FusedLocationApi until the Interval is changed.
 *
 * @return locationRequest
 */
private LocationRequest buildLocationRequest() {
    int dateConversion = 1000;
    LocationRequest locationRequest = LocationRequest.create();
    locationRequest.setInterval(mIntentInterval * dateConversion);
    locationRequest.setFastestInterval((mIntentInterval / 2) * dateConversion);
    locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
    Log.w(TAG, "Building location request");
    return locationRequest;
}

EDIT After a long discussion in chat with Catherine, we came to the conclusion that google play services library 7.5 has a bug that does not process the Parcelable Extra Location passed from FusedLocationProviderAPI when other extras are put into the Intent. However, 7.0 does provide this capability. She said that she will submit a bug and we'll see how long it takes the Android team to resolve

andrewdleach
  • 2,458
  • 2
  • 17
  • 25
  • > Using the IntentService coupled with the FusedLocationProviderAPI will present issues. Where did you get this information? I looked and looked and looked through the Android Documentation for the fused location provider and IntentService and neither said that they are incompatible or that their combination could cause trouble.. Maybe Google's docs _do_ suck... :'( – techjeffharris Aug 15 '15 at 17:47
  • > So, the method requires an implementation of LocationListener for foreground updates, or a PendingIntent for background updates coupled with a Broadcast Receiver. Again, I never saw anything in the docs that said a broadcast receiver was required. Would you please explain why or point me to a piece of documentation that explains why the broadcast receiver must be used. – techjeffharris Aug 15 '15 at 17:57
  • Look [here](http://developer.android.com/training/location/receive-location-updates.html) under the Request Location Updates heading in the second paragraph. It's in there. – andrewdleach Aug 15 '15 at 20:47
  • Further, you don't strictly need a BroadcastReceiver, just some instance of a class to handle the updates when they're issued. BroadcastReceiver worked well in my implementation because it required WAKE BOOT broadcast too. – andrewdleach Aug 15 '15 at 21:01
  • It has been my understanding that, as you said, any instance of a class may handle the intent, but you haven't answered my question regarding why a person will run into issues when using an `IntentService`. – techjeffharris Aug 15 '15 at 21:17
  • Issues will arise because the API specifies a PendingIntent or a LocationListener to handle Location Updates. A PendingIntent is used for extending permissions for another piece of code (FusedLocationProviderAPI in Google Play Services) to execute their code within your apk. An IntentService is used to start a Service defined within the scope of your apk. – andrewdleach Aug 15 '15 at 21:26
  • Okie dokie. I was not aware of that distinction. Thank you! – techjeffharris Aug 15 '15 at 22:36
  • Not a problem. Glad to help – andrewdleach Aug 15 '15 at 22:37
  • Tested the solution. Seems to have the same problem as the original--i.e. when I get the intent in the receiver, the extras are present, but the location is null. – Catherine Aug 17 '15 at 19:53
  • @Catherine are you testing on an emulator or on a device ? – andrewdleach Aug 17 '15 at 19:56
  • I'm testing on a device. – Catherine Aug 17 '15 at 20:12
  • 1
    @Catherine, I forgot to include an example of LocationRequest creation in the first answer posted. This may help. The only other thing I can think of is maybe GPS is not 'on' on the device? – andrewdleach Aug 17 '15 at 20:53
  • 1
    @andrewdleach The GPS is definitely on. When I include extras in the intent, I get no location, if I take out the extras, I get the location. – Catherine Aug 17 '15 at 21:34
  • @Catherine gosh...I'm stumped. I can verify this worked for me and my team. If you want to clone a repo and compare you could go to https://github.com/leachad/buuteeq-ponyhax – andrewdleach Aug 17 '15 at 21:36
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/87205/discussion-between-catherine-and-andrewdleach). – Catherine Aug 17 '15 at 22:04
  • Was a bug ever raised for this? This still happens on version 8.x of Google Play Services, so having to roll back to 7.0.0. – cockadoodledo Oct 05 '15 at 10:00
  • This is still happening on version 9.x of Google Play Services. @andrewdleach There is no reason that an `IntentService` shouldn't work. In fact, it does work. It is perfectly acceptable to use an `IntentService` with a `PendingIntent`, and always has been. – Eliezer Jun 22 '16 at 08:52
  • 1
    The problem persists even in version 10.x – user3105453 Feb 18 '17 at 20:17
  • I'm so glad I stopped screwing around with this issue. Obviously would have led to a year of watching updates with no relief. :-/ – techjeffharris Mar 13 '17 at 23:21