Hello, and welcome back to our series on Android development with Fabric and PubNub! In previous articles, we’ve shown how to create a powerful chat app with Android, Fabric, and Digits. In this blog entry, we highlight 2 key technologies, Twitter Fabric (mobile development toolkit) and MapBox Kit for Fabric (a world-class open source mapping toolkit). With these technologies, we can accelerate mobile app development and build an app with several real-time data features that you will be able to use as-is or employ easily in your own data streaming applications:
PubNub provides a global real-time Data Stream Network with extremely high availability and low latency. With PubNub, it’s possible for data exchange between devices (and/or sensors, servers, you name it – essentially anything that can talk TCP) in 100ms worldwide. And of that 100ms, a large part comes from the last hop – the data carrier itself! As 4G LTE (5G won’t be far away) and cloud computing gain traction, those latencies will decrease even further.
Twitter Fabric is a development toolkit for Android (as well as iOS and some Web capabilities) that gives developers a powerful array of options:
MapBox is a high-quality SDK on the Fabric platform enabling easy integration of API mapping capabilities (iOS, Python, and HTML/JavaScript implementations are also available). In our application, this will allow us to create a new UI that displays all online users on a dynamic map in real time.
This may seem like a lot to digest. How do all these things fit together exactly?
As you can see in the animated GIF above, once everything is together, we have built an application very quickly that provides a great feature set with relatively little code and integration pain. This includes:
This all seems pretty sweet, so let’s move on to the development side…
If you haven’t already, you’ll want to create a Fabric account like this:
You should be on your way in 60 seconds or less!
In Android studio, as you know, everything starts out by creating a new Project.
In our case, we’ve done much of the work for you – you can jumpstart development with the sample app by downloading it from GitHub, or the “clone project from GitHub” feature in Android Studio if prefer. The Git url for the sample app is:
https://github.com/sunnygleason/pubnub-android-fabric-chat-ext.git
Once you have the code, you’ll want to create a Fabric Account if you haven’t already.
Then, you can integrate the Fabric Plugin according to the instructions you’re given. The interface in Android Studio should look something like this, under Preferences > Plugins > Browse Repositories:
Once everything’s set, you’ll see the happy Fabric Plugin on the right-hand panel:
Click the “power button” to get started, and you’re on your way!
Adding MapBox is an easy 4-step process:
Here’s a visual overview of what that looks like:
Adding PubNub is just as easy:
Look familiar? That’s the beauty of Fabric!
Using this same process, you can integrate over a dozen different toolkits and services with Fabric.
This article builds on the sample application described in a previous article in the series. If you would like more information about the core features and implementation, please feel free to check it out! There is also a pre-recorded training webinar available, as well as ongoing live webinars!
Once you’ve set up the sample application, you’ll want to update the publish and subscribe keys in the Constants
class, your Twitter API keys in the MainActivity
class, your Fabric API key in the AndroidManifest.xml
, and MapBox API key in strings.xml
. These are the keys you created when you made a new PubNub and MapBox accounts and PubNub application in previous steps. Make sure to update these keys, or the app won’t work!
Here’s what we’re talking about in the Constants
class:
package com.pubnub.example.android.fabric.pnfabricchat; public class Constants { ... public static final String PUBLISH_KEY = "YOUR_PUBLISH_KEY"; // replace with your PN PUB KEY public static final String SUBSCRIBE_KEY = "YOUR_SUBSCRIBE_KEY"; // replace with your PN SUB KEY ... }
These values are used to initialize the connection to PubNub when the user logs in.
And in the MainActivity
:
public class MainActivity extends AppCompatActivity { private static final String TWITTER_KEY = "YOUR_TWITTER_KEY"; private static final String TWITTER_SECRET = "YOUR_TWITTER_SECRET"; ... }
These values are necessary for the user authentication feature in the sample application.
And in the AndroidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.pubnub.example.android.fabric.pnfabricchat"> ... <application ...> ... <meta-data android:name="io.fabric.ApiKey" android:value="YOUR_API_KEY" /> ... </application> ... </manifest>
This is used by the Fabric toolkit to integrate features into the application.
Here’s where to integrate MapBox in the strings.xml
:
<resources> ... <string name="com.mapbox.mapboxsdk.accessToken" translatable="false">YOUR_MAPBOX_TOKEN</string> ... </resources>
As with any Android app, there are 2 main portions of the project – the Android code (written in Java), and the resource files (written in XML).
The Java code contains 2 Activities, plus packages for each major feature: chat, presence, and mapbox. (The speech
package is for another article on dictation and text-to-speech features – check it out!)
The resource XML files include layouts for each activity, fragments for the 2 tabs, list row layouts for each data type, and a menu definition with a single option for “logout”.
Whatever you need to do to modify this app, chances are you’ll just need to tweak some Java code or resources. In rare cases, you might add some additional dependencies in the build.gradle
file, or modify permissions or behavior in the AndroidManifest.xml
.
In the Java code, there is a package for each of the main features:
For ease of understanding, there is a common structure to each of these packages that we’ll dive into shortly.
The Android manifest is very straightforward – we need 3 permissions (INTERNET, ACCESS_FINE_LOCATION, and ACCESS_NETWORK_STATE), and have 2 activities: LoginActivity (for login), and MainActivity (for the main application).
You’ll also need to enable the Telemetry
service for MapBox to work.
The XML is available here and described in the previous article.
Our application uses several layouts to render the application:
LoginActivity
and MainActivity
.Chat
, Presence
, and PresenceMap
.ListView
, Chat
and Presence
.These are all standard layouts that we pieced together from the Android developer guide, but we’ll go over them all just for the sake of completeness.
The login activity layout is pretty simple – it’s just one button for the Twitter login, and one button for the super-awesome Digits auth.
The XML is available here and described in the previous article.
It results in a layout that looks like this:
The Main Activity features a tab bar and view pager – this is pretty much the standard layout suggested by the Android developer docs for a tab-based, swipe-enabled view:
The XML is available here and described in the previous article.
It results in a layout that looks like this:
Ok, now that we have our top-level views, let’s dive into the tab fragments.
The presence map tab layout features a dynamic map using the MapBox Map View.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.mapbox.mapboxsdk.maps.MapView android:id="@+id/mapboxMarkerMapView" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>
Pretty easy indeed! It creates a UI that looks like this:
In the code that follows, we’ve categorized things into a few areas for ease of explanation. Some of these are standard Java/Android patterns, and some of them are just tricks we used to follow PubNub or other APIs more easily.
sender
field is represented by an TextView
in the UI).That might seem like a lot to take in, but hopefully as we go into the code it should feel a lot easier.
The LoginActivity
is pretty basic – we just include code for instantiating the view and setting up Digits login callbacks. If you look at the actual source code, you’ll also notice code to support Twitter auth as well.
The Java code is available here and described in the previous article.
We attach the login event to a callback with two outcomes: the success callback, which extracts the phone number and moves on to the MainActivity
to display a Toast message; and the error callback, which does nothing but Log (for now).
In a real application, you’d probably want to use the Digits user ID from the digitsSession
to link it to a user account in the backend.
There’s a lot going on in the MainActivity
. This makes sense, since it’s the place where the application is initialized and where UI event handlers live. Take a moment to glance through the code and we’ll talk about it below. We’ve removed a bunch of code to highlight the portions that are used for our dynamic location and mapping services.
public class MainActivity extends AppCompatActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { ... this.mLocationHelper = new LocationHelper(this, getLocationListener()); ... this.mPresenceMapAdapter = new PresenceMapAdapter(this); ... this.mPresenceCallback = new PresencePnCallback(this.mPresenceListAdapter, this.mPresenceMapAdapter); ... adapter.setPresenceMapAdapter(this.mPresenceMapAdapter); viewPager.setAdapter(adapter); viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); ... initPubNub(); initChannels(); } ... @Override protected void onStart() { super.onStart(); if (this.mLocationHelper != null) { this.mLocationHelper.connect(); } } @Override protected void onStop() { super.onStop(); if (this.mLocationHelper != null) { this.mLocationHelper.disconnect(); } } ... private final LocationListener getLocationListener() { return new LocationListener() { @Override public void onLocationChanged(final Location newLocation) { JSONObject location = new JSONObject(); if (newLocation != null) { location.tryPut("lat", String.valueOf(newLocation.getLatitude())); location.tryPut("lon", String.valueOf(newLocation.getLongitude())); } MainActivity.this.mPubnub.setState(Constants.CHANNEL_NAME, MainActivity.this.mUsername, location, new Callback() { @Override public void successCallback(String channel, Object message) { Log.v("setState", channel + ":" + message); mPresenceMapAdapter.update(new PresenceMapPojo(mUsername, newLocation.getLatitude(), newLocation.getLongitude(), DateTimeUtil.getTimeStampUtc())); } }); } }; } ... private final void initChannels() { ... this.mPubnub.hereNow(Constants.CHANNEL_NAME, true, true, this.mPresenceCallback); ... } ... }
The first thing you’ll notice in this code is that we create a LocationHelper
instance, which is our helper code to bridge between Google Play Location Services and our dynamic mapping feature. We instantiate the LocationHelper with a reference to the Activity context, as well as a LocationListener instance to receive location update events.
The most important things happening in the onCreate()
method with respect to the speech features are as follows:
PresenceMapAdapter
, which will be responsible for translating location and presence events into map update events.PresenceMapAdapter
into the PresencePnCallback
so that it can receive location state change events from PubNub.In addition, we’ll need to add code to:
setState()
method and update the PresenceMapAdapter accordingly.hereNow()
call to ask for uuid
and state
information (the 2 true
booleans in the hereNow()
call).Stay tuned for more description of the location and mapping helpers below.
The Java code for the chat and presence features is available here and here and described in the previous article.
The Pojo
classes are the most straightforward of the entire app – they are just immutable objects that hold data values as they come in. We make sure to give them toString()
, hashCode()
, and equals()
methods so they play nicely with Java collections.
The RowUi
object just aggregates the UI elements in a list row. Right now, these just happen to be TextView
instances.
The TabFragment
object takes care of instantiating the tab and hooking up the ListAdapter.
The PnCallback
is the bridge between the PubNub client and our application logic. It takes an inbound messageObject
object and turns it into a Pojo value that is forwarded on to the ListAdapter
instance.
The PresenceMapPojo
is very similar to the PresencePojo
, except that it contains Double instances for latitude and longitude instead of a presence state.
public class PresenceMapPojo { private final String sender; private final Double lat; private final Double lon; private final String timestamp; ... }
The PresenceMapTabFragment
class is a little bigger than usual because we’re initializing the MapBox Map View. We create references to the MapView and MapboxMap so we can initialize the PresenceMapAdapter at the appropriate time. The MapView is the overall Map view implementation that integrates into the Android UI. The MapboxMap is the object we will interact with to add location markers for each user.
public class PresenceMapTabFragment extends Fragment { private PresenceMapAdapter presenceMapAdapter; private AtomicReference<MapView> mapViewRef = new AtomicReference<MapView>(); private AtomicReference<MapboxMap> mapboxMapRef = new AtomicReference<MapboxMap>(); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_presence_map, container, false); MapView mapView = (MapView) view.findViewById(R.id.mapboxMarkerMapView); mapViewRef.set(mapView); mapView.setAccessToken(getString(R.string.com_mapbox_mapboxsdk_accessToken)); mapView.onCreate(savedInstanceState); mapView.getMapAsync(new OnMapReadyCallback() { @Override public void onMapReady(MapboxMap mapboxMap) { mapboxMap.setStyle(Style.MAPBOX_STREETS); mapboxMapRef.set(mapboxMap); if (presenceMapAdapter != null) { presenceMapAdapter.refreshAll(); } } }); return view; } public void setAdapter(PresenceMapAdapter presenceMapAdapter) { this.presenceMapAdapter = presenceMapAdapter; presenceMapAdapter.setMapView(mapViewRef, mapboxMapRef); } ... }
The PresenceMapAdapter
follows the Android Adapter
pattern, which is used to bridge data between Java data collections and user interfaces (although in this case, we’re bridging to a map view instead of a list view). Since we’re using PubNub, messages are coming in all the time, unexpected from the point of view of the UI. This adapter is invoked from the PresencePnCallback
class: when a presence event comes in, the callback invokes PresenceMapAdapter.update()
with a PresenceMapPojo object containing the relevant data.
In the case of the PresenceMapAdapter
, the backing collections are maps of uuid
to PresenceMapPojo
and Map MarkerView
instances, so the update()
and refresh()
calls need to:
put(uuid, value)
).We use AtomicReference instances since the Map objects are initialized at different times in the application. The MapboxMap instance is created asynchronously in the Tab Fragment class when the MapView is initialized.
Not too bad!
public class PresenceMapAdapter { private final Context context; private final Map<String, MarkerView> latestMarker = new LinkedHashMap<>(); private final Map<String, PresenceMapPojo> latestPresence = new LinkedHashMap<>(); private AtomicReference<MapView> mapViewRef; private AtomicReference<MapboxMap> mapboxMapRef; public PresenceMapAdapter(Context context) { this.context = context; } public void setMapView(AtomicReference<MapView> mapViewRef, AtomicReference<MapboxMap> mapboxMapRef) { this.mapViewRef = mapViewRef; this.mapboxMapRef = mapboxMapRef; } public void update(final PresenceMapPojo message) { ... latestPresence.put(message.getSender(), message); if (mapboxMapRef.get() == null) { return; } ((Activity) this.context).runOnUiThread(new Runnable() { @Override public void run() { if (latestMarker.containsKey(message.getSender())) { mapboxMapRef.get().removeMarker(latestMarker.get(message.getSender())); latestMarker.remove(message.getSender()); } MarkerViewOptions markerOptions = new MarkerViewOptions() .position(new LatLng(message.getLat(), message.getLon())) .title(message.getSender()) .snippet(message.getTimestamp()); MarkerView marker = mapboxMapRef.get().addMarker(markerOptions); latestMarker.put(message.getSender(), marker); } }); } public void refresh(final PresencePojo message) { ... String presence = message.getPresence(); if ("timeout".equals(presence) || "leave".equals(presence)) { latestPresence.remove(message.getSender()); if (mapboxMapRef.get() == null) { return; } if (latestMarker.containsKey(message.getSender())) { ((Activity) this.context).runOnUiThread(new Runnable() { @Override public void run() { mapboxMapRef.get().removeMarker(latestMarker.get(message.getSender())); latestMarker.remove(message.getSender()); } }); } } } public void refreshAll() { for (PresenceMapPojo message : latestPresence.values()) { update(message); } } }
The PresencePnCallback
features a bunch of changes from the version in the previous article. The main difference is that we’re using the custom state API for propagating user location information. When presence events come in, we look for the “state” attribute (sometimes also called “data”) and update the corresponding user location accordingly. When “leave” or “timeout” events occur, we propagate null
location events to remove the user location marker from the map.
public class PresencePnCallback extends Callback { ... @Override public void successCallback(String channel, Object message) { ... try { Map<String, Object> presence = JsonUtil.fromJSONObject((JSONObject) message, LinkedHashMap.class); List<Map<String, Object>> uuids; if (presence.containsKey("uuids")) { uuids = (List<Map<String, Object>>) presence.get("uuids"); } else { uuids = ImmutableList.<Map<String, Object>>of(presence); } for (Map<String, Object> object : uuids) { ... if (object.containsKey("data") || object.containsKey("state")) { // we have a state change if (presenceMapAdapter != null) { Log.v(TAG, "presenceStateChange(" + JsonUtil.asJson(presence) + ")"); if ("timeout".equals(presenceString) || "leave".equals(presenceString)) { presenceMapAdapter.refresh(pm); } else { Map<String, Object> state = object.containsKey("data") ? (Map<String, Object>) object.get("data") : (Map<String, Object>) object.get("state"); ; if (state.containsKey("lat") && state.containsKey("lon")) { Double lat = Double.parseDouble((String) state.get("lat").toString()); Double lon = Double.parseDouble((String) state.get("lon").toString()); presenceMapAdapter.update(new PresenceMapPojo(sender, lat, lon, timestamp)); } } } } ... } } catch (Exception e) { throw Throwables.propagate(e); } } ... }
The code is a little trickier than necessary because we’re using the same callback to send events to the PresenceListAdapter
and PresenceMapAdapter
instances. All in all though, it’s not too tough to wire everything together!
The location update feature uses Google Play Location Services, which has a friendly API to work with. There are a multitude of callbacks to implement for these APIs, which is the main reason why we broke out a LocationHelper
class instead of implementing them in the MainActivity
class.
public class LocationHelper implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { private GoogleApiClient mGoogleApiClient; private LocationListener mLocationListener; 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; } public void connect() { this.mGoogleApiClient.connect(); } public void disconnect() { this.mGoogleApiClient.disconnect(); } @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 Throwables.propagate(e); } mLocationListener.onLocationChanged(location); } @Override public void onConnectionSuspended(int i) { mLocationListener.onLocationChanged(null); } }
The implementation initializes Location Services, asks for the last known location, and requests dynamic location updates from the Google Play Location API. When we receive location change events, we forward them on to the LocationListener instance that we were initialized with. In this case, it’s the LocationListener created in the MainActivity that calls PubNub.setState()
with the new location. Not too shabby!
In a real-world implementation, you’ll probably want to pay close attention to your location accuracy and power utilization. More updates equals more battery waste, so be frugal!
Thank you so much for staying with us this far! Hopefully it’s been a useful experience. The goal was to convey our experience in how to build an app that can:
If you’ve been successful thus far, you shouldn’t have any trouble extending the app to any of your real-time data processing needs.
Stay tuned, and please reach out anytime if you feel especially inspired or need any help!
There are common underlying technologies for a dating app, and in this post, we’ll talk about the major technologies and designs...
Michael Carroll
How to use geohashing, JavaScript, Google Maps API, and BART API to build a real-time public transit schedule app.
Michael Carroll
How to track and stream real-time vehicle location on a live-updating map using EON, JavaScript, and the Mapbox API.
Michael Carroll