Build

Location – Android Geolocation Tracking w/ Google Maps API

Michael Carroll on Jun 29, 2019
Location – Android Geolocation Tracking w/ Google Maps API

This is Part Three of our four-part series on building real-time maps with geolocation tracking using the Google Maps API and PubNub.

What is Geolocation Publishing?

In this tutorial, we'll live-update our map markers we built in Part Two with live geolocation capabilities. We'll use the Google Play Services Location API to collect the user's location from their device, and stream and publish location changes (based on the device’s self-reported location) to the map using PubNub Real-time Messaging.

pubnub_android_location

Tutorial Overview

The code for this example is available in our GitHub repository here.

If you haven't already, you first need to take care of a couple of prerequisites we covered in Part One and Two, where we set up our Android environment and got started with map markers.

Now that we have all of the relevant configuration settings we took care of in Part One, and we have our map markers, let's get started with collecting and publishing location data.

Android Activities

As previously stated, the MainActivity file is responsible for collecting the username preference, creating the tabbed view (including three tabs), and initializing the PubNub library for real-time communications. In this tutorial, we’ll focus on the second tab, which is responsible for displaying a live updating map marker with the device’s location.

In this case, the LocationPublishTabContentFragment has handlers for fragment creation and Google Map initialization. We want to make sure that the view is using the correct layout, and that fragment is properly passed in for getMapAsync callback handler.

public class LocationPublishTabContentFragment extends Fragment implements OnMapReadyCallback, LocationListener {
    ...
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_locationpublish, container, false);
        SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager()
               .findFragmentById(R.id.map_locationpublish);
        mapFragment.getMapAsync(this);
        return view;
    }
    ...

The Google Maps initialization callback function populates the map attribute with the initialized GoogleMap object and takes care of subscribing to PubNub message events pertaining to location updates. We create a LocationHelper instance to bridge from the Android location services API to our application’s location change handling logic.

...
@Override
public void onMapReady(GoogleMap map) {
    this.map = map;
    this.pubNub.addListener(new LocationSubscribePnCallback(new LocationSubscribeMapAdapter((Activity) this.getContext(), map), Constants.PUBLISH_CHANNEL_NAME));
    this.pubNub.subscribe().channels(Arrays.asList(Constants.PUBLISH_CHANNEL_NAME)).execute();
    this.locationHelper = new LocationHelper((Activity) this.getContext(), this);
}
...

In the code above, you see two primary interactions with the PubNub API. We register a listener with the real-time data streams to receive events (from one or more channels), and then subscribe to a specific channel (or channels) to receive updates.

PubNub Events

There are two key things to note about the PubNub subscription and listener. We create a LocationPublishPnCallback object to satisfy the PubNub callback API. We create a LocationPublishMapAdapter to handle updates to the UI.

The reason for this separation is that the PubNub events are coming in asynchronously, whereas UI updates need to be propagated and queued for running on the main UI thread.

The LocationPublishPnCallback is event-driven and has no dynamic state – it just parses messages (only from the relevant channel) and propagates logical events to the Adapter class.

public class LocationPublishPnCallback extends SubscribeCallback {
    ...
    @Override
    public void message(PubNub pubnub, PNMessageResult message) {
        if (!message.getChannel().equals(watchChannel)) {
            return;
        }
        try {
            Log.d(TAG, "message: " + message.toString());
            Map<String, String> newLocation = JsonUtil.fromJson(message.getMessage().toString(), LinkedHashMap.class);
            locationMapAdapter.locationUpdated(newLocation);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    ...
}

The LocationPublishMapAdapter receives the incoming events and updates the UI accordingly. For the sake of simplicity, we use a java.util.Map to pass data between the PubNub callback and the Adapter.

In a larger example, one should consider using a specialized value object (bean) for type safety and enforce the contract between PnCallback and Adapter. The key thing to remember in this code is the need to perform UI updates in the UI thread – we reduce the amount of code in the doUiUpdate method so that it processes as quickly as possible.

public class LocationPublishMapAdapter {
    ...
    public void locationUpdated(final Map<String, String> newLocation) {
        if (newLocation.containsKey("lat") && newLocation.containsKey("lng")) {
            String lat = newLocation.get("lat");
            String lng = newLocation.get("lng");
            doUiUpdate(new LatLng(Double.parseDouble(lat), Double.parseDouble(lng)));
        } else {
            Log.w(TAG, "message ignored: " + newLocation.toString());
        }
    }
    private void doUiUpdate(final LatLng location) {
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (marker != null) {
                    marker.setPosition(location);
                } else {
                    marker = map.addMarker(new MarkerOptions().position(location));
                }
                map.moveCamera(CameraUpdateFactory.newLatLng(location));
            }
        });
    }
}

At this point, we have integrated real-time location updates into our Android application. For reference, we’ll also show you the implementation of the LocationHelper class that bridges between Android location services and our application.

Google Play Services Location API

In the code below, we set up a LocationHelper class for bridging between the Google Play Services Location API and our application code.

Google Play location services has a bunch of API-specific callbacks, so we use the LocationHelper class to meld them all into a single “location updated” callback method that may receive a null location (in case of error, disconnection, etc.). The main idea for integration is to create a Google Play Services API client, connect it, and respond to the different callbacks that it may invoke.

In our case, we process the location updated events and dispatch them to the LocationListener that the LocationHelper was instantiated with. We request location updates at a frequency of 5s (5000ms); your application may have a different location update frequency.

public class LocationHelper implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener {
    ...
    public LocationHelper(Context context, LocationListener mLocationListener) {
        this.mGoogleApiClient = new GoogleApiClient.Builder(context)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(LocationServices.API)
                .build();
        this.mGoogleApiClient.connect();
        this.mLocationListener = mLocationListener;
    }
    ...
    @Override
    public void onConnected(@Nullable Bundle bundle) {
        try {
            Location lastLocation = LocationServices.FusedLocationApi.getLastLocation(
                    mGoogleApiClient);
            if (lastLocation != null) {
                onLocationChanged(lastLocation);
            }
        } catch (SecurityException e) {
            Log.v("locationDenied", e.getMessage());
        }
        try {
            LocationRequest locationRequest = LocationRequest.create().setInterval(5000);
            LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, locationRequest, this);
        } catch (SecurityException e) {
            Log.v("locationDenied", e.getMessage());
        }
    }
    ...
    @Override
    public void onLocationChanged(Location location) {
        try {
            Log.v("locationChanged", JsonUtil.asJson(location));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        mLocationListener.onLocationChanged(location);
    }
    ...
}

Next Steps

We're 75% of the way finished, and now it's time to add our final feature in Part Four, flight paths! Flight paths will show what route our device has taken by drawing a trail behind the map marker.