Build Your Own Uber/Lyft App with Geolocation Tracking

PubNub Developer Relations - Feb 22 - - Dev Community

In this post from our archives, we explain how to build your own Uber or Lyft clone, using PubNub to track the vehicle position. Whilst some of the code snippets might reference older library versions, everything in this article is appropriate for today's audience

Tracking the real-time location of another device is the basis of many popular mobile, location-focused apps, especially when it comes to building

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.

Uber/Lyft App

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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"/>
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

Main Activity

The MainActivity class will have three important functionalities:

  1. Directing our user to their respective interface, depending on whether they are a driver or passenger.
  2. Establishing a connection to PubNub for all users of the app
  3. 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:

screenshot

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));
   }
});
Enter fullscreen mode Exit fullscreen mode

Next, using our PubNub credentials from the Admin Dashboard, we will create a

 

PNConfiguration
Enter fullscreen mode Exit fullscreen mode

 instance, which allows us to create a

PubNub
Enter fullscreen mode Exit fullscreen mode


 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()
Enter fullscreen mode Exit fullscreen mode


 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);
    }
Enter fullscreen mode Exit fullscreen mode

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);
   }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

 class from the Google Location API in order to request location updates from the driver. We will also use the

 

LocationRequest
Enter fullscreen mode Exit fullscreen mode


 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);
Enter fullscreen mode Exit fullscreen mode

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();
...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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());
           }
       });
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

Then, instantiate the

 

MapFragment
Enter fullscreen mode Exit fullscreen mode

 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
Enter fullscreen mode Exit fullscreen mode


 method by having PassengerActivity implement

onMapReadyCallback
Enter fullscreen mode Exit fullscreen mode

. Then we will pass

 

PassengerActivity.this
Enter fullscreen mode Exit fullscreen mode


 as a parameter into our MapFragment object’s

 

getMapAsync
Enter fullscreen mode Exit fullscreen mode


 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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

, 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
Enter fullscreen mode Exit fullscreen mode

 class, which contains the method

 

fromJson()
Enter fullscreen mode Exit fullscreen mode


 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()
Enter fullscreen mode Exit fullscreen mode


 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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

 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()
Enter fullscreen mode Exit fullscreen mode

. 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)));
        }
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

interface below. This allows us to implement the method

interpolate()
Enter fullscreen mode Exit fullscreen mode

, 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()
Enter fullscreen mode Exit fullscreen mode

. We will call this method from a

 

ValueAnimator
Enter fullscreen mode Exit fullscreen mode

instance, which we get obtain from the

 

onAnimationUpdate()
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode
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);
           }
       }
   }
Enter fullscreen mode Exit fullscreen mode

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.

If the embedded content is not available on this page, it can also be viewed at https://www.youtube.com/embed/kQ6EzqoQu5A

More Resources

How can PubNub help you?

This article was originally published on PubNub.com

Our platform helps developers build, deliver, and manage real-time interactivity for web apps, mobile apps, and IoT devices.

The foundation of our platform is the industry's largest and most scalable real-time edge messaging network. With over 15 points-of-presence worldwide supporting 800 million monthly active users, and 99.999% reliability, you'll never have to worry about outages, concurrency limits, or any latency issues caused by traffic spikes.

Experience PubNub

Check out Live Tour to understand the essential concepts behind every PubNub-powered app in less than 5 minutes

Get Setup

Sign up for a PubNub account for immediate access to PubNub keys for free

Get Started

The PubNub docs will get you up and running, regardless of your use case or SDK

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .