Build

Android Geolocation Tracking with Google Maps API (2/4)

Michael Carroll on Jul 29, 2019
Android Geolocation Tracking with Google Maps API (2/4)

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

What are Map Markers?

In this tutorial, we'll add map markers to our Android map. Map markers identify a location on a map through a customizable icon that is overlayed on the map. To showcase map marker functionality, we'll place a single map marker and update its location on the fly based on random positions within a bounding box.

pubnub_android_mapmarker

Tutorial Overview

If you haven't already, you first need to take care of a couple of prerequisites we covered in Part One. These include:

  • Creating your PubNub application, including Publish and Subscribe Keys
  • Creating your Google Maps API Project and Credentials

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

Android Activities

Now that we have all of the relevant configuration settings we took care of in Part One, we can look at the actual working code.

The MainActivity file is responsible for collecting the username preference, creating the tabbed view (including 3 tabs), and initializing the PubNub library for real-time communications. In this tutorial, we’ll focus on the first tab, which is responsible for displaying a live updating Map marker.

public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSharedPrefs = getSharedPreferences(Constants.DATASTREAM_PREFS, MODE_PRIVATE);
        if (!mSharedPrefs.contains(Constants.DATASTREAM_UUID)) {
            Intent toLogin = new Intent(this, LoginActivity.class);
            startActivity(toLogin);
            return;
        }
        setContentView(R.layout.activity_main);
        this.random = new Random();
        this.userName = mSharedPrefs.getString(Constants.DATASTREAM_UUID, "anonymous_" + random.nextInt(10000));
        this.pubNub = initPubNub(this.userName);
        TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
        tabLayout.addTab(tabLayout.newTab().setText("1_Marker"));
        tabLayout.addTab(tabLayout.newTab().setText("2_Live_Location"));
        tabLayout.addTab(tabLayout.newTab().setText("3_Flightpath"));
        tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
        final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
        final MainActivityTabManager adapter = new MainActivityTabManager
                (getSupportFragmentManager(), tabLayout.getTabCount(), pubNub);
        viewPager.setAdapter(adapter);
        viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                viewPager.setCurrentItem(tab.getPosition());
            }
            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
            }
            @Override
            public void onTabReselected(TabLayout.Tab tab) {
            }
        });
    }
    @NonNull
    private PubNub initPubNub(String userName) {
        PNConfiguration pnConfiguration = new PNConfiguration();
        pnConfiguration.setPublishKey(Constants.PUBNUB_PUBLISH_KEY);
        pnConfiguration.setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY);
        pnConfiguration.setSecure(true);
        pnConfiguration.setUuid(userName);
        return new PubNub(pnConfiguration);
    }
}

When we create the TabManager (a container for the three tab fragments), we pass in the PubNub object so that each tab may be initialized properly.

In this case, the LocationSubscribeTabContentFragment 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 LocationSubscribeTabContentFragment extends Fragment implements OnMapReadyCallback {
    ...
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_locationsubscribe, container, false);
        SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager()
                .findFragmentById(R.id.map_locationsubscribe);
        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 also invoke a custom scheduleRandomUpdates() method that periodically publishes new location updates to the channel.

In a real-world application, the data source would likely be different – in many cases, location updates will be published by a backend system or another mobile client entirely.

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

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 LocationSubscribePnCallback object to satisfy the PubNub callback API.
  • We create a LocationSubscribeMapAdapter 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 LocationSubscribePnCallback 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 LocationSubscribePnCallback extends SubscribeCallback {
    ...
    @Override
    public void message(PubNub pubnub, PNMessageResult message) {
        if (!message.getChannel().equals(watchChannel)) {
            return;
        }
        try {
            Log.d(TAG + "/PN_MESSAGE", "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 LocationSubscribeMapAdapter 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 LocationSubscribeMapAdapter {
    ...
    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 scheduleRandomUpdates() method that publishes the location updates to which the application subscribes.

Location Publish Functionality

In the code below, we set up a single-threaded repeating task to publish updates to the channel. Each message contains a new latitude and longitude pair that is moving northeast (increasing latitude and longitude).

In your application, you would likely have a different mechanism for determining location – via Android location API, or a user’s self-reported location for example (which will cover in Part Three).

public class LocationSubscribeTabContentFragment extends Fragment implements OnMapReadyCallback {
    ...
    private static ImmutableMap<String, String> getNewLocationMessage(String userName, int randomLat, int randomLng, long elapsedTime) {
        String newLat = Double.toString(generatorOriginLat + ((randomLat + elapsedTime) * 0.000003));
        String newLng = Double.toString(generatorOriginLng + ((randomLng + elapsedTime) * 0.00001));
        return ImmutableMap.<String, String>of("who", userName, "lat", newLat, "lng", newLng);
    }
    ...
    private void scheduleRandomUpdates() {
        ...
        this.executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                ((Activity) LocationSubscribeTabContentFragment.this.getContext()).runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        int randomLat = random.nextInt(10);
                        int randomLng = random.nextInt(10);
                        long elapsedTime = System.currentTimeMillis() - startTime;
                        final Map<String, String> message = getNewLocationMessage(userName, randomLat, randomLng, elapsedTime);
                        pubNub.publish().channel(Constants.SUBSCRIBE_CHANNEL_NAME).message(message).async(
                                new PNCallback<PNPublishResult>() {
                                    @Override
                                    public void onResponse(PNPublishResult result, PNStatus status) {
                                        try {
                                            if (!status.isError()) {
                                                Log.v(TAG, "publish(" + JsonUtil.asJson(result) + ")");
                                            } else {
                                                Log.v(TAG, "publishErr(" + JsonUtil.asJson(status) + ")");
                                            }
                                        } catch (Exception e) {
                                            e.printStackTrace();
                                        }
                                    }
                                }
                        );
                    }
                });
            }
        }, 0, 5, TimeUnit.SECONDS);
    }
}

Next Steps

With that, we now have our Android application, and we're able to plot map markers. The next step is to detect the device's location and plot it in real time, moving it as the location changes, which we'll cover in Part 3 on geolocation!