Nowadays, mobile fleet management solutions and custom mobile application development services are becoming increasingly popular. Taxi driving, trucking, and even service companies include smartphones/tablets in their processes because of their many benefits. Using smart devices helps optimize the route, plan the service or delivery schedule, and, thus, reduce operational costs.
Fleet management software offers a broad range of functionalities, controlling everything from on-time maintenance to fuel consumption, but the main benefit is to control the vehicle movement.
The base functionality of the fleet management app is as follows:
- show a map;
- place the current location of the vehicle;
- place the destination point;
- draw the route between these two points;
- smoothly replace the vehicle position when it is moving;
- show the direction in which the vehicle is moving;
- hide the route passed by a vehicle.
Of course, this functionality can be improved. For example, the passed distance between start and end of travel can be calculated, and values such as speed can also be measured.
In this article, we will show you the sample app that can be used as a base functionality for car tracking fleet management apps. Note that this is the very base example; you can feel free to adapt it to your needs. Just consider this as a template to start developing something great.
Development part: analyzing the source code
First of all, you should generate the google_maps_key to use the map. Also add the map fragment into your layout. This is basic Android knowledge; there is a tutorial from google about that and a lot of others, so we can skip this one.
When the map is ready to use and the onMapReady() callback is triggered, we are placing the default marker that will be our end point. We are also binding and starting the service that will track locations in the background and call the callback when the location is changed.
@Override
public void onMapReady(GoogleMap googleMap) {
map = googleMap;
map.getUiSettings().setMapToolbarEnabled(false);
LatLng sydney = new LatLng(-33.870814, 151.206997);
toMarker = map.addMarker(new MarkerOptions().position(sydney));
map.moveCamera(CameraUpdateFactory.newLatLngZoom(sydney, 17));
doServiceBind();
startService(new Intent(this, LocationService.class));
}
There is a doServiceBind() method that involves sіmply checking if service was not bound and, if not, binding it. The doServiceUnbind() method is the same but reversed and also removes the location update callback.
private void doServiceBind(){
if (!isServiceBound){
Intent intent = new Intent(this, LocationService.class);
bindService(intent, this, BIND_AUTO_CREATE);
isServiceBound = true;
}
}
private void doServiceUnbind(){
if (isServiceBound){
unbindService(this);
locationService.removeLocationCallback();
isServiceBound = false;
}
}
Location callback is added when service has been successfully bound.
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
LocationService.LocationBinder binder = (LocationService.LocationBinder) iBinder;
if (locationService == null){
locationService = binder.getService();
locationService.addLocationCallback(this);
}
}
Let’s take a closer look at LocationService. On onCreate(), we are initializing the location manager.
@Override
public void onCreate() {
initializeLocationManager();
}
private void initializeLocationManager() {
if (mLocationManager == null) {
mLocationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
}
}
In the onStartCommand() method, we are requesting location updates from both NETWORK_PROVIDER and GPS_PROVIDER.
LocationListener[] mLocationListeners = new LocationListener[] {
new LocationListener(LocationManager.GPS_PROVIDER),
new LocationListener(LocationManager.NETWORK_PROVIDER)
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
try {
mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, LOCATION_INTERVAL, 0, mLocationListeners[1]);
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOCATION_INTERVAL, 0, mLocationListeners[0]);
} catch (java.lang.SecurityException ex) {
Log.i(TAG, "fail to request location update, ignore", ex);
} catch (IllegalArgumentException ex) {
Log.d(TAG, "network provider does not exist, " + ex.getMessage());
}
return START_STICKY;
}
LOCATION_INTERVAL is a constant that means how often we will receive a new location. For our app, it is 2 seconds.
private static final int LOCATION_INTERVAL = 2000;
LocationListener is the custom listener that implements native location listener and calls the callback in onLocationChanged().
private class LocationListener implements
android.location.LocationListener {
Location mLastLocation;
public LocationListener(String provider) {
Log.d(TAG, "LocationListener " + provider);
mLastLocation = new Location(provider);
}
@Override
public void onLocationChanged(Location location) {
Log.d(TAG, "onLocationChanged: " + location);
if (mLastLocation != null){
mLastLocation = location;
}
if (locationCallback!=null){
locationCallback.onLocationChanged(location);
}
}
Let's return to handling the location change. Here is the code that we have in onLocationChanged():
@Override
public void onLocationChanged(Location newLocation) {
showVehicleMarker(newLocation);
if (driverDestination == null && !isDriverDestLoading){
LatLng origin = new LatLng( newLocation.getLatitude(), newLocation.getLongitude());
loadDestination(origin, toMarker.getPosition());
}
handleDestination(driverDestination, newLocation);
if (currentLocation.distanceTo(newLocation) >= 1.5){
float bearingTo = currentLocation.bearingTo(newLocation);
MarkerAnimation.rotateMarker(vehicleMarker, bearingTo);
}
LatLng destination = new LatLng(newLocation.getLatitude(), newLocation.getLongitude());
MarkerAnimation.animateMarker(vehicleMarker, destination, new LatLngInterpolator.Linear());
currentLocation = newLocation;
}
Let’s check all the methods step by step. The showVehicleMarker() method takes the current location and places the marker on the map. It triggers one time.
private void showVehicleMarker(Location newLocation) {
if (vehicleMarker == null){
currentLocation = newLocation;
LatLng currentPosition = new LatLng(newLocation.getLatitude(), newLocation.getLongitude());
MarkerOptions currentPosMarker = createMarker(currentPosition, R.mipmap.ic_vehicle);
vehicleMarker = map.addMarker(currentPosMarker);
}
}
The createMarker() method is just a helper method that creates a Marker object based on position and icon.
private MarkerOptions createMarker(LatLng point, int icResId) {
MarkerOptions options = new MarkerOptions();
options.position(point);
options.icon(BitmapDescriptorFactory.fromResource(icResId));
options.anchor(0.5f, 0.5f);
options.flat(true);
return options;
}
The next step is loading the destination from the current position to the end point. This method needs to be triggered one more time.
private void loadDestination(LatLng from, LatLng to) {
isDriverDestLoading = true;
LoadDirectionTask loadDirectionTask = new LoadDirectionTask(this,from, to);
loadDirectionTask.execute();
}
LoadDirectionTask is an AsyncTask that loads the destination from the Google Maps API. It takes the direction-loaded callback, start position and end position as parameters. It generates the request, parses the response, and trickers the callback when it is loaded. We prefer to skip the source of this class because it is pretty complicated. You can research this class in the sample project if you want; it will be attached to the article.
Let’s check what happens when the direction is loaded.
@Override
public void onDirectionLoaded(List<LatLng> points) {
isDriverDestLoading = false;
driverDestination = drawDestination(points);
LatLngBounds.Builder builder = new LatLngBounds.Builder();
builder.include(vehicleMarker.getPosition());
builder.include(toMarker.getPosition());
LatLngBounds bounds = builder.build();
map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100));
}
private Polyline drawDestination(List<LatLng> points){
PolylineOptions lineOptions = new PolylineOptions();
lineOptions.addAll(points);
lineOptions.width(7);
lineOptions.color(Color.parseColor("#003c61"));
lineOptions.geodesic(true);
return map.addPolyline(lineOptions);
}
First, we draw the destination on the map by using the drawDestination() method. The next lines just focus the map so we can see the end point and the vehicle position.
Location updates
The next step is handling the location updates and modifying the destination line. On the UI, we will see that the destination line behind the car maker disappears. This path is already passed, and we don’t need to show it.
private void handleDestination(Polyline destination, Location theLocation) {
if (destination==null) {
return;
}
List<LatLng> points = destination.getPoints();
LatLng currentPosition = new LatLng(theLocation.getLatitude(), theLocation.getLongitude());
int indexOnPath = PolyUtil.locationIndexOnPath(currentPosition, points, true, 100);
if (indexOnPath > 0){
List<LatLng> latLngs = points.subList(indexOnPath, points.size());
destination.setPoints(latLngs);
} else if (indexOnPath < 0){
points.add(0, currentPosition);
destination.setPoints(points);
}
}
PolyUtil is the helper class taken from android-maps-utils. The locationIndexOnPath() method helps us to know if the driver is driving past the presented destination or not. It returns us to the index of the point from the points list. If it is bigger than zero, it means that the driver passed some points, and we should take the sublist from the original destination; the rest of the points are not needed anymore (the driver passed them, and we are hiding it). If it is smaller than zero, then it means that the driver is not at the destination, and we add the position to the original destination and place them on the map. Notice that the PolyUtil class is using MathUtil and SphericalUtil, which are also taken from android-maps-utils.
Regarding the locationIndexOnPath() method parameters, let’s look on the 100 value. It is the tolerance parameter in meters. In other words, it specifies the radius that we should consider when determining the passed path. For example, if the driver drives according to the presented destination but on a wide road and changes lanes, we should still erase the path behind the car. In another case, if tolerance is 0, the app will draw a new line and add it to the existing path.
After that, we rotate the car marker to show where the driver turned the car.
if (currentLocation.distanceTo(newLocation) >= 1.5){
float bearingTo = currentLocation.bearingTo(newLocation);
MarkerAnimation.rotateMarker(vehicleMarker, bearingTo);
}
The "if" condition is used to prevent handling every location. It makes the rotation animation smoother.
The last part is changing the car position with the animation and saving the last position.
LatLng destination = new LatLng(newLocation.getLatitude(), newLocation.getLongitude());
MarkerAnimation.animateMarker(vehicleMarker, destination, new LatLngInterpolator.Linear());
currentLocation = newLocation;
We override the finish() method to unbind and stop service when closing the app.
@Override
public void finish() {
super.finish();
stopService(new Intent(this, LocationService.class));
doServiceUnbind();
}
Takeaways
Fleet management software helps companies take more precise control over their resources, thus reducing operational costs. Controlling vehicle movement in real time is a basic feature of any fleet management solution. Once you have it done, you might add more advanced features to your app like fuel consumption calculation or maintenance planning.
Check our mobile development services or contact us to discuss your fleet management project.