IN THIS ARTICLE
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 realtime data features that you will be able to use as-is or employ easily in your own data streaming applications:
- Send chat messages to a PubNub channel (Chat) : using the PubNub plugin for realtime messaging and Publish/Subscribe API.
- Display current and historical chat messages (Chat) : using the PubNub plugin and History API.
- Display a list of what users are online (Presence) : using the PubNub plugin and Presence API.
- Display users on a real-world map (Presence Map) : using the MapBox Android API.
- Log in with Digits auth (Login) : using the Digits plugin for Fabric allowing the easiest mobile user authentication possible.
PubNub provides a global realtime 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 less than a quarter of a second worldwide. And of that 250ms, 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:
- Familiar dev toolkit for iOS, Android and Web Applications : a unified developer experience that focuses on ease of development and maintenance.
- Brings best-of-breed SDKs all in one place : taking the pain out of third-party SDK provisioning and using new services in your application.
- Streamlined dependency management : Fabric plugin kits are managed together to consolidate dependencies and avoid “dependency hell”.
- Rapid application development with task-based sample code onboarding : You can access and integrate sample code use cases right from the IDE.
- Automated Key Provisioning : sick of creating and managing yet another account? So were we! Fabric will provision those API keys for you.
- Open Source, allowing easier understanding, extension and fixes.
MapBox is a high-quality SDK on the Fabric platform enabling easy integration of 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 realtime.
This may seem like a lot to digest. How do all these things fit together exactly?
- We are building an Android app because we want to reach the most devices worldwide.
- We use the Fabric plugin for Android Studio, giving us our “mission control” for plugin adoption and app releases.
- We adopt Best-of-Breed services (like PubNub) rapidly by quickly integrating plugin kits and sample code in Fabric.
- We use PubNub as our Global Realtime Data Stream Network to power the Chat and Presence features.
- In addition, we’ll use MapBox kit for Fabric to provide the best mapping capabilities possible.
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:
- Log in with Digits phone-based authentication (or your own alternative login mechanism).
- Send & receive chat messages (or whatever structured realtime data you like).
- Show a list of users online (or devices/sensors/vehicles, etc.).
- Display users on a dynamic world map.
This all seems pretty sweet, so let’s move on to the development side…
Sign up with Fabric
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!
Android Studio
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!
MapBox SDK Integration
Adding MapBox is an easy 4-step process:
- Click to Install from the list of Fabric kits
- Enter your MapBox keys or have Fabric create a new account
- Integrate any Sample Code you need to get started
- Launch the App to verify successful integration… and that’s it!
Here’s a visual overview of what that looks like:
PubNub SDK Integration
Adding PubNub is just as easy:
- Click to Install from the list of Fabric kits
- Enter your PubNub keys or have Fabric create a new account
- Integrate any Sample Code you need to get started
- Launch the App to verify successful integration
Look familiar? That’s the beauty of Fabric!
Using this same process, you can integrate over a dozen different toolkits and services with Fabric.
Additional Background Information
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!
Navigating the Code
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:
- chat : code related to implementing the realtime chat feature.
- presence : code related to implementing the online presence list of users.
- mapbox : helper code for working with the dynamic map UI.
For ease of understanding, there is a common structure to each of these packages that we’ll dive into shortly.
Android Manifest
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.
Layouts
Our application uses several layouts to render the application:
- Activity : the top-level layouts for
LoginActivity
andMainActivity
. - Fragment : layouts for our the tabs,
Chat
,Presence
, andPresenceMap
. - Row Item : layouts for the the types of
ListView
,Chat
andPresence
.
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.
Activity Layouts
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:
Fragment Layouts
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:
Java Code
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.
- Activity : these are the high-level views of the application, the Java code provides initialization and UI element event handling.
- Pojo : these are Plain Old Java Objects representing the “pure data” that flows from the network into our application.
- Fragment : these are the Java classes that handle instantiation of the UI tabs.
- RowUi : these are the corresponding UI element views of the Pojo classes (for example, the
sender
field is represented by anTextView
in the UI). - PnCallback : these classes handle incoming PubNub data events (for publish/subscribe messaging and presence).
- Adapter : these classes accept the data from inbound data events and translate them into a form that is useful to 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.
LoginActivity
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.
MainActivity
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:
- Create a
PresenceMapAdapter
, which will be responsible for translating location and presence events into map update events. - Pass the
PresenceMapAdapter
into thePresencePnCallback
so that it can receive location state change events from PubNub. - Set the PresenceMapAdapter within the PresenceMapTabFragment via the fragment manager (since it’s in charge of instantiating the fragments itself).
In addition, we’ll need to add code to:
- Start/stop location services during those Android application events.
- Publish location updates via PubNub
setState()
method and update the PresenceMapAdapter accordingly. - Modify our usual PubNub
hereNow()
call to ask foruuid
andstate
information (the 2true
booleans in thehereNow()
call).
Stay tuned for more description of the location and mapping helpers below.
Chat and Presence Features
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.
PresenceMapPojo
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; ... }
PresenceMapTabFragment
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); } ... }
PresenceMapAdapter
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:
- Update the items in the collection (actually prepend by doing
put(uuid, value)
). - Create, update, or remove the map marker view accordingly (this should happen on the UI thread).
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); } } }
PresencePnCallback
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!
Location Helper
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!
Conclusion
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:
- Authenticate with Twitter or Digits auth.
- Send chat messages to a PubNub channel.
- Display current and historical chat messages.
- Display a list of what users are online.
- Display a dynamic map of online users.
If you’ve been successful thus far, you shouldn’t have any trouble extending the app to any of your realtime data processing needs.
Stay tuned, and please reach out anytime if you feel especially inspired or need any help!
Resources
- https://admin.pubnub.com/#/register
- https://www.pubnub.com/docs
- https://www.pubnub.com/docs/android-java/pubnub-java-sdk
- https://www.pubnub.com/docs/java-se-java/pubnub-java-sdk
- https://www.pubnub.com/products/realtime-messaging/
- https://www.pubnub.com/products/presence/
- https://www.pubnub.com/products/storage-and-playback/
- https://www.pubnub.com/blog/pubnub-coming-soon-to-twitter-fabric-platform/
- https://developer.android.com/training/location/index.html
- https://developers.google.com/android/reference/com/google/android/gms/location/package-summary
- https://www.mapbox.com/android-sdk/
- https://www.mapbox.com/android-sdk/api/4.2.0-beta.2/com/mapbox/mapboxsdk/maps/MapboxMap.html
- https://www.mapbox.com/android-sdk/api/4.2.0-beta.2/com/mapbox/mapboxsdk/maps/MapView.html
- https://fabric.io
- https://fabric.io/kits/android/
- https://fabric.io/kits/android/mapbox
- https://fabric.io/kits/android/pubnub
- https://fabric.io/kits/android/twitterkit