Tutorial: How to build your own p2p chat app (like Couple)


Introduction

Couple is a shared diary for you and the one you love. We're releasing it as an open source Android app (iOS coming soon) that demonstrates the Hipmob platform's user-to-user messaging libraries. Here, we provide the published mobile app from which users can invite a friend, the source code, and a simple server-side framework that manages the friend lists for each user to ensure that the only person who can send messages to them (and who a user can send messages to) is the person they are coupled with. This tutorial shows you how to build one of your own!
Get Couple from the Google Play Store.
Download the code for this tutorial to follow along:
In this tutorial we will:
  1. Provide an overview of the Couple functionality.
  2. Discuss the concepts used in the application (which would be typical for any user-to-user chat application built on the Hipmob platform).
  3. Review the options for user identification.
  4. Go over managing the friend lists for a specific user.
  5. Show how to launch a conversation with a specific user.
  6. Cover push notification integration so users are notified of messages even when they are not in the app.

Overview

We'll walk through setting up Femi and his oh-so-close friend Femo as a couple. First, Femi installs the app on his Android device from the Google Play store and then launches it: he sees the lovely splash screen.

Figure 1: splash screen

He can then login (if he already has an account) using an email address and a password.

Figure 2: login screen

Since this is his first time with Couple, Femi taps the Don't have an account? link and switches to the signup screen.

Figure 3: signup screen

He then signs up with his name, email address and a new password.

Figure 4: completed signup screen

He waits.

Figure 5: signing up

And, he's in. Since he just signed up, he's prompted to invite that special someone (in this case, Femo).

Figure 6: main settings screen

He types in Femo's email address (never mind that it is suspiciously similar to his own).

Figure 7: inviting that special someone

After tapping the Invite button, he waits.

Figure 8: sending invitation

And, its sent.

Figure 9: invitation sent

Now, Femo will get an email showing he's been invited.

Figure 10: email invitation

Since Femo really really wants to talk to Femi, he installs the app and then signs up.

Figure 11: Femo signing up

Once Femo signs up, he's prompted to Couple!

Figure 12: Couple request

Of course he agrees.

Figure 13: accepting request

And, he's coupled up!

Figure 14: Femo is now coupled to Femi

And a few seconds later, Femi receives the happy news.

Figure 15: Femo has accepted Femi's request to Couple

Femi can now hit the Chat button and start talking to Femo.

Figure 16: Femi chatting with Femo

Femo will receive a push notification that he has messages waiting: Femo doesn't need to wait to receive the messages.

Figure 17: push notification alert

And once he taps the push notification he'll receive the messages.

Figure 18: Femi's messages delivered

Femo can now reply to Femi.

Figure 19: Femo replying to Femi

And, Femi receives them instantly.

Figure 20: Femi gets the good word back

And that's it.

Concepts

Couple uses an email address as its server account identifier; this allows us send invitations to people who may not yet have the app installed (via email).

When a user first signs up (through the mobile app), we use a custom Node JS server to create a user identifier which is passed back to the mobile app and stored: this user identifier is the same one passed to the Hipmob platform and is used to identify the user.

Whenever a user sends an invitation (by email) the Node JS server checks if the person who was invited already has an account, and if they do we send them a Couple request that they can accept or decline. If they don't have an account we send an email and keep the request on ice until the person who was invited signs up: whenever they sign up we can connect them to the request.

When a user receives a request they can choose to accept it (in which case the person who sent the request is notified that they were successful) or decline it (in which case the person who sent the request is notified that they were turned down). We also send the user identifiers (and names) to the mobile app.

If the user accepts the request we use the Hipmob API to connect the 2 users together by updating their friend lists: this prevents spam messages or anyone not explicitly approved from sending messages to another user.

In the mobile app, once the user identifiers have been received the Chat button is displayed. Tapping on that button launches a Hipmob chat window using the previously received user identifiers.

Hipmob uses the concept of applications to separate users: with the Hipmob management interface you create applications and then use the application's ID to connect to the Hipmob platform. Connections made to the Hipmob platform with a specific key can only communicate with other connections that use the same key.

Hipmob can be used for both user-to-user chat AND for support chat: the Support button on the top of the Settings page will open up a chat to the designated application operators: users can then have conversations with the operators.

As part of the setup for the mobile app we use Hipmob's push notification support to collect Google Cloud Messaging tokens and send them to the Hipmob platform: whenever a user receives a message and is not currently logged in they'll receive a push notification to their device. In addition to GCM, Hipmob also supports push notifications using Parse. Click here for a tutorial.

User Identification

When a user logs in or signs up in the mobile app, we send an HTTP POST to the Node JS server with the email address and password (you can see this in the CoupleLogin and CoupleSignup classes):
StringBuilder sb = new StringBuilder(300);
sb.append("email=").append(URLEncoder.encode(email));
sb.append("&password=").append(URLEncoder.encode(password));

String res = App.executePost(App.SERVER_URI+"login", sb.toString(), ua.toString());
if(res != null){
JSONObject detail = (JSONObject) new JSONTokener(res).nextValue();
if(detail.has("error")){
m.what = FAILURE;
m.obj = detail.getString("error");
}else if(detail.has("guid") && detail.has("name")){
m.what = SUCCESS;
m.obj = detail;
}
}
The Node JS server uses a Postgres database and uses Heroku server and database hosting. The object model is pretty simple: there's a Person (which represents an individual signup) and a Request (which represents a request by one user to Couple with another user) (you can see the Postgres DDL here). The important parts are shown below:

Person

guid : String

first_name: String

email: String

password: String

Request

guid : String

requester: String

requested: String

email: String

accepted: Date

declined: Date

cancelled: Date

When a user signs up, the guid from the Person entity created for you is returned and stored by the Android app: this is the user identifier used by the Hipmob platform.

Friend Lists

Every connection to the Hipmob platform is attached to a specific user: the mobile app can specify a user identifier that is then used by the Hipmob platform to tie all activity on the connection back to a specific user within the mobile app. Couple passes the guid from the Person to the Hipmob platform, and the Hipmob platform automatically creates and maintains a friend list for that guid: this friend list is initially empty but can be updated using the Hipmob REST API. The Hipmob Node JS library allows us to easily manage the friend list from the server: whenever a Couple request is accepted we set the friend list of the user who made the request to only include the user who accepted the request (you can see the Node JS code here:
var hipmob = require("hipmob");
var handle = hipmob(process.env.HIPMOB_USERNAME, process.env.HIPMOB_PASSWORD);
var dev = handle.get_device(process.env.HIPMOB_APPID, row.requester_guid, false);
var friendlist = [handle.get_device(process.env.HIPMOB_APPID, row.requested_guid, false)];
dev.set_friends(friendlist, function(err, count){
  if(err){
    console.error("[link.accept;5]"+err);
    console.error(err.stack);
    response.end(JSON.stringify({ 'error': 'A Hipmob service error occured: please try again later.' }));
  }else{
    response.end(JSON.stringify({ 'accepted': true, 'partnerGuid': row.requester_guid, 'partnerEmail': row.requester_email, 'partnerName': row.requester_name }));
  }
});
Adding user #1 to user #2's friend list automatically add's user #2 to user #1's friend list: only one call is required.
And whenever a Couple request is cancelled, we clear the friend list:
dev.remove_all_friends(function(err, count){
  if(err){
    console.error("[link.breakup;4]"+err);
    console.error(err.stack);
    response.end(JSON.stringify({ 'error': 'A Hipmob service error occured: please try again later.' }));
  }else{
    response.end(JSON.stringify({ 'success': true }));
  }
});
As before, only one call is required: the removal from user #1's friend list automatically removes from #user #2's friend list.

Chatting

Once the friend lists are configured right, we can simply start up a chat from one user to the other by passing the guids of the person starting the chat and the intended recipent (you can see this in the startChat method of CoupleSettings):
void startChat(){
  String email = prefs.getString(getString(R.string.pref_email), "");
  if ("".equals(email)) {
    Toast.makeText(this, getString(R.string.error_invalid_email),
    Toast.LENGTH_LONG).show();
    return;
  }

  String name = prefs.getString(getString(R.string.pref_name), "");
  if ("".equals(name)) {
    Toast.makeText(this, getString(R.string.error_invalid_name),
    Toast.LENGTH_LONG).show();
    return;
  }

  String peer = prefs.getString(getString(R.string.pref_partner), "");

  // start the chat
  Intent i = new Intent(this, HipmobCore.class);

  // REQUIRED: set the appid to the key you're provided
  i.putExtra(HipmobCore.KEY_APPID, App.HIPMOB_KEY);

  // provide the device identifier here (for use with API calls and
  // for peer-to-peer connections)
  i.putExtra(HipmobCore.KEY_DEVICEID, guid);

  // set the user's name here
  i.putExtra(HipmobCore.KEY_NAME, name);

  // put the user's email here (will show up in the chat status)
  i.putExtra(HipmobCore.KEY_EMAIL, email);

  // and finally the peer
  i.putExtra(HipmobCore.KEY_PEER, peer);

  // title
  i.putExtra(HipmobCore.KEY_TITLE, String.format(getString(R.string.title_chat), prefs.getString(getString(R.string.pref_partner_name), "")));

  startActivity(i);
}
KEY_DEVICEID is set to the guid of the user who logged in, and KEY_PEER is set to the guid of the couple, and we just launch a chat window.
If we want to open a support chat window, the startSupportChat method does it in almost exactly the same way, just leaves out the KEY_PEER section:
void startSupportChat(){
  String email = prefs.getString(getString(R.string.pref_email), "");
  if ("".equals(email)) {
    Toast.makeText(this, getString(R.string.error_invalid_email),
    Toast.LENGTH_LONG).show();
    return;
  }

  String name = prefs.getString(getString(R.string.pref_name), "");
  if ("".equals(name)) {
    Toast.makeText(this, getString(R.string.error_invalid_name),
    Toast.LENGTH_LONG).show();
    return;
  }

  // start the chat
  Intent i = new Intent(this, HipmobCore.class);

  // REQUIRED: set the appid to the key you're provided
  i.putExtra(HipmobCore.KEY_APPID, App.HIPMOB_KEY);

  // provide the device identifier here (for use with API calls and
  // for peer-to-peer connections)
  i.putExtra(HipmobCore.KEY_DEVICEID, guid);

  // set the user's name here
  i.putExtra(HipmobCore.KEY_NAME, name);

  // put the user's email here (will show up in the chat status)
  i.putExtra(HipmobCore.KEY_EMAIL, email);

  // title
  i.putExtra(HipmobCore.KEY_TITLE, getString(R.string.title_support_chat));

  startActivity(i);
}

Push Notifications

Push notifications make it easy for Hipmob to keep a user informed whenever they aren't logged in and a message is received. We first have to add the required fields in the AndroidManifest.xml to setup the Google Cloud Messaging bits:
<service android:name="com.hipmob.android.HipmobPushService" />
<meta-data
            android:name="hipmob_push_sender_ids"
            android:resource="@string/sender_ids" />
<meta-data
            android:name="hipmob_notification_target"
            android:resource="@string/notification_target" />
<meta-data
            android:name="hipmob_notification_text"
            android:resource="@string/notification_message" />
Once that is done Hipmob cleanly integrates with Android's Google Cloud Messaging: in the splash Activity's onCreate we setup Hipmob's push notification integration:
@Override
protected void onCreate(Bundle savedInstanceState)
{
  // Be sure to call the super class.
  requestWindowFeature(Window.FEATURE_NO_TITLE);
  super.onCreate(savedInstanceState);

  HipmobPushService.setup(this);
}
And a push notification token is created and stored locally: upon the first connection to the Hipmob platform the push notification token is sent to the Hipmob platform. Any subsequent messages (either user-to-user or support) to the user of the app with this token will then trigger a push notification (if the user is offline).

Feedback

And..we're done. Thanks for following along: don't hesitate to get in touch with us over email (team at hipmob.com) or chat (hit the handy tab at the bottom right of this page) if you have any questions or suggestion. Check out our documentation for a more complete list of all the things that Hipmob can do for you.