IN THIS ARTICLE
Tracking the realtime location of another device is the basis of many popular mobile, location-focused apps, specifically in the ridesharing industry for leaders like Lyft and Uber. Building this functionality is challenging and can be tedious, but we’ll simplify it with an easy publish/subscribe model between the drivers and passengers, powered by PubNub.
Tutorial Overview
Full source code for this tutorial is available here.
In this tutorial, we’ll show you how to implement this common use case in an Android application. The sample app we will build first asks the user if they are the driver or the passenger. If a user is a driver, their current location will be published to a channel, updated every 5 seconds (in this sample), or however many times your app requires.
If a user is a passenger, they will subscribe to the same channel to receive updates on the current location of their driver. This publish/subscribe model is displayed in the visual below.
Google Maps API and PubNub Setup
We first need to write our Gradle dependencies to use the PubNub Android SDK and Google Play Services for the map functionality. We also will implement com.fasterxml.jackson
libraries, in order to complete some basic JSON parsing of PubNub messages.
In the build.gradle file for the app module under dependencies, input the codeblock of command below to ensure that we have what we need. Be sure that Google Play Services is installed from the Android SDK Manager on the tools portion of the menu, so we may implement the necessary libraries.
implementation group: 'com.pubnub', name: 'pubnub-gson', version: '4.12.0' implementation 'com.google.android.gms:play-services-maps:15.0.1' implementation "com.google.android.gms:play-services-location:15.0.1" implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.2' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.2' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.2'
Now that we have the necessary dependencies, let’s make sure that we have the necessary permissions in our Android Manifest file. With the permission to access network state and establish internet connection, the user can now make the connection to the PubNub API. We will use the location permission later, when we request for the driver’s location.
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
In order to set up PubNub in your Android app, you will need to create a PubNub app in the PubNub Admin Dashboard (it’s free). Upon creating the app, you will be assigned a subscribe key and publish key. In the following part, we will use these credentials in our MainActivity class to establish the connection.
Now, we must set up our Google Maps API key, so that we can use it in our application. In order to do this head over to Google’s Developer API console. Here, we will create a new project and then generate an API key. Now that we have our API key, we can write the following code to set up our Google Maps API and configure it with our generated API key.
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> <meta-data android:name="com.google.android.geo.API_KEY" android:value=”ENTER_KEY_HERE" />
Main Activity
The MainActivity class will have three important functionalities:
- Directing our user to their respective interface, depending on whether they are a driver or passenger.
- Establishing a connection to PubNub for all users of the app
- Checking location access permissions
First, we want to separate our users into our drivers and passengers and provide them with their respective user interface. We will do this by creating a DriverActivity class and a PassengerActivity class, which we will discuss in detail in parts 2 and parts 3.
In MainActivity we will make a simple button-based interface which prompts the user to let the app know their role as a driver or passenger. Using Android’s Intent class, we can transition from the MainActivity class into the DriverActivity class or from the MainActivity class into the PassengerActivity class, depending on which button is pressed.
The interface will appear as:
driverButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this, DriverActivity.class)); } }); passengerButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this, PassengerActivity.class)); } });
Next, using our PubNub credentials from the Admin Dashboard, we will create a PNConfiguration
instance, which allows us to create a PubNub
instance connecting our app and PubNub. This PubNub instance in the MainActivity class will be public and static so it is accessible from both the DriverActivity and the PassengerActivity. We will call the below method in our onCreate()
method of the Main Activity, so that PubNub is properly initialized for all of our app’s users. This is all shown in the following code.
private void initPubnub() { PNConfiguration pnConfiguration = new PNConfiguration(); pnConfiguration.setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY); pnConfiguration.setPublishKey(Constants.PUBNUB_PUBLISH_KEY); pnConfiguration.setSecure(true); pubnub = new PubNub(pnConfiguration); }
Finally, we must ensure that our user has enabled their location services for our app. This can be done with the following code. If our user does not grant permission to access fine location or coarse location, our app will request this permission.
public void checkPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED ) {//Can add more as per requirement ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, 123); } }
Driver Activity
The sole function of the DriverActivity class is to obtain the driver’s location and publish this data at a certain time interval to the PubNub channel.
First, let’s look at how to make a location request. We will use the FusedLocationProviderClient
class from the Google Location API in order to request location updates from the driver. We will also use the LocationRequest
class in order to specify important parameters of our location request, such as the priority, smallest displacement, and time interval.
For our sample, we will use a time interval of 5000 milliseconds, so that the location is updated every 5 seconds and a smallest displacement of 10 meters to ensure that the location is only being updated if there is a change of at least 10 meters. Finally, we want the location request to use the mobile device’s GPS, for high accuracy location tracking. This high accuracy is important in enabling the passenger to see a detailed display of their driver (shown as car icon) moving across the map to the destination.
LocationRequest locationRequest = LocationRequest.create(); locationRequest.setInterval(5000); locationRequest.setFastestInterval(5000); locationRequest.setSmallestDisplacement(10); locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
Now that we have defined our location request parameters, we will pass this LocationRequest object into our FusedLocationProviderClient object’s request for location updates.
mFusedLocationClient.requestLocationUpdates(locationRequest, new LocationCallback() { @Override public void onLocationResult(LocationResult locationResult) { Location location = locationResult.getLastLocation(); ...
Using the location we get from our request, we must convert it into a LinkedHashMap so that it fits the desired format of a location message in the PubNub channel. The LinkedHashMap will have two keys “lat” and “lng” and the corresponding values for these keys, both as Strings.
Now with our message in the format of a LinkedHashMap, we are able to publish it to the PubNub channel. This is shown in the following code. Notice that we must use MainActivity.pubnub
to obtain the PubNub instance that we earlier in the onCreate method of the Main Activity. We do not want to establish multiple connections by initializing PubNub again. Instead, we just use the one we already created in the MainActivity class.
MainActivity.pubnub.publish() .message(message) .channel(Constants.PUBNUB_CHANNEL_NAME) .async(new PNCallback<PNPublishResult>() { @Override public void onResponse(PNPublishResult result, PNStatus status) { // handle publish result, status always present, result if successful // status.isError() to see if error happened if (!status.isError()) { System.out.println("pub timetoken: " + result.getTimetoken()); } System.out.println("pub status code: " + status.getStatusCode()); } });
Passenger Activity
In our PassengerActivity class, we want to create a MapView to show the location of the driver being updated. In order to add the MapView UI element to our Passenger Activity we must include the following code in our corresponding layout.xml file for this activity.
<fragment xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/map" android:name="com.google.android.gms.maps.SupportMapFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.mapwithmarker.MapsMarkerActivity" />
Then, instantiate the MapFragment
object using the ID we define in the layout file. In our sample code, we used “map” as the ID. We will also set up our onMapReady
method by having PassengerActivity implement onMapReadyCallback
. Then we will pass PassengerActivity.this
as a parameter into our MapFragment object’s getMapAsync
method. This allows us to setup our onMapReady callback method for our MapFragment object.
mMapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.map); mMapFragment.getMapAsync(PassengerActivity.this);
Now, we want to subscribe to the driver location channel so we receive updates on the driver’s current location. This must be done once the map is ready, so we will include this code in our callback method. Additionally, we must add a listener with a SubscribeCallback
, so our app knows how to act upon receiving a message.
As specified in the PubNub Android docs, we must carry this out in the order where we first add the listener and then subscribe the PubNub instance to the channel. Once we receive a message, we want to convert the JSON output into a LinkedHashMap, which we can easily parse to obtain the longitude and latitude. In order to convert the JSON output into a LinkedHashMap, we must import the JsonUtil
class, which contains the method fromJson()
that allows us to do this. This class is shown in the full source code. Once we have the driver’s location, we must update the UI by calling our method updateUI()
and passing the new location as the parameter.
MainActivity.pubnub.addListener(new SubscribeCallback() { @Override public void status(PubNub pub, PNStatus status) { } @Override public void message(PubNub pub, final PNMessageResult message) { runOnUiThread(new Runnable() { @Override public void run() { try { Map<String, String> newLocation = JsonUtil.fromJson(message.getMessage().toString(), LinkedHashMap.class); updateUI(newLocation); } catch (Exception e) { e.printStackTrace(); } } }); } @Override public void presence(PubNub pub, PNPresenceEventResult presence) { } }); MainActivity.pubnub.subscribe() .channels(Arrays.asList(Constants.PUBNUB_CHANNEL_NAME)) // subscribe to channels .execute();
Our updateUI method has two cases it will encounter.
The first case is that there does not already exist a marker for the driver and in this case, we will instantiate the Marker
object and set its position to the first tracked location of the driver. We must also zoom in on the map in order to maintain a street view of the driver’s car.
The second case is that there already exists a marker for the driver and in this case, we do not have to re-instantiate the Marker object. Rather, we simply move the location of the marker across the map to its new location. We do this by calling the method animateCar()
. We must also ensure that we move the camera of the MapView to the new location, if it is out of the bounds of the camera. Thus, the driver’s marker will always be in the bounds of the MapView. This is shown in the following code.
private void updateUI(Map<String, String> newLoc) { LatLng newLocation = new LatLng(Double.valueOf(newLoc.get("lat")), Double.valueOf(newLoc.get("lng"))); if (driverMarker != null) { animateCar(newLocation); boolean contains = mGoogleMap.getProjection() .getVisibleRegion() .latLngBounds .contains(newLocation); if (!contains) { mGoogleMap.moveCamera(CameraUpdateFactory.newLatLng(newLocation)); } } else { mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom( newLocation, 15.5f)); driverMarker = mGoogleMap.addMarker(new MarkerOptions().position(newLocation). icon(BitmapDescriptorFactory.fromResource(R.drawable.car))); } }
In our animateCar method, we must move our car marker to its new location, in a smooth line shaped manner. The animation must last 5 seconds, because our app receives updates on the driver’s location every 5 seconds. This way every time the animation is over, it will have the new location update to start the next animation. That way, we minimize the lag in the animation of the car.
We will also utilize the LatLngInterpolator
interface below. This allows us to implement the method interpolate()
, which returns a new LatLng coordinate that’s a fraction of the way to the final newLocation. This fraction is determined with the method getAnimatedFraction()
. We will call this method from a ValueAnimator
instance, which we get obtain from the onAnimationUpdate()
callback method. This fraction will increase at a steady rate so that by 5 seconds, it is 1 (all the way to the new location). This animating of the car is shown in the following code snippets.
private void animateCar(final LatLng destination) { final LatLng startPosition = driverMarker.getPosition(); final LatLng endPosition = new LatLng(destination.latitude, destination.longitude); final LatLngInterpolator latLngInterpolator = new LatLngInterpolator.LinearFixed(); ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.setDuration(5000); // duration 5 seconds valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { try { float v = animation.getAnimatedFraction(); LatLng newPosition = latLngInterpolator.interpolate(v, startPosition, endPosition); driverMarker.setPosition(newPosition); } catch (Exception ex) { } } }); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); } }); valueAnimator.start(); }
private interface LatLngInterpolator { LatLng interpolate(float fraction, LatLng a, LatLng b); class LinearFixed implements LatLngInterpolator { @Override public LatLng interpolate(float fraction, LatLng a, LatLng b) { double lat = (b.latitude - a.latitude) * fraction + a.latitude; double lngDelta = b.longitude - a.longitude; if (Math.abs(lngDelta) > 180) { lngDelta -= Math.signum(lngDelta) * 360; } double lng = lngDelta * fraction + a.longitude; return new LatLng(lat, lng); } } }
Further Improvements
If you wish to further develop this sample, one feature to add would be having the marker turn as the car turns. This feature would require us to add one key-value pair to our message structure. We would need to include the key “bearing” and the value. Then we would simply rotate the marker depending on the driver’s bearing.
Congrats!
Now that we have created the MainActivity, DriverActivity, and PassengerActivity, we have successfully built the publish/subscribe model necessary to create a simple Lyft/Uber Android demo. Click here for the full source code.