In this tutorial, we are going to build a typing indicator in an Android chat application using Pusher. A basic knowledge of how to build Android applications is assumed in this tutorial and we'll be focusing on the implementation of the typing indicator of the Android application.
The chat application will be simple. First, we will build a simple Node.js server that will receive requests from Android when a user is typing. The server will then broadcast this to everyone as a Pusher event.
Then we will go ahead to build the Android application. When a user starts typing in the edit text field of the Android app, the app sends a request to the server. The Android app will also subscribe to the typing events from Pusher and show a 'user is typing' message when a broadcast is received.
We will be using Pusher for the realtime features of this chat application, so the first step is to create your Pusher account. You can do this at https://pusher.com/signup. When you first log in, a pop-up dialogue appears as shown below:
If you already have an account, log in to the Pusher dashboard and click on the Create new app
button in the Your apps
to the left. Select 'Android' for the front-end tech and 'Node.js' for the backend tech. (The tech stack you select now doesn't matter as you can always change it later. Its purpose is to generate the starter code that you will need to start communicating with Pusher.)
After creating the new app, go to the App Keys
tab and copy your App ID, Key, and Secret credentials. We will use them later in the tutorial.
Now that you have your Pusher Keys, let's get on with building the chat application server.
First, generate a Node.js application using this command:
npm init -y
Next, install Express, Pusher and some other dependencies the server will be needing:
npm install --save express body-parser pusher
Express is the web server library that we will be using to accept HTTP requests from the Android app when the user starts typing, and body-parser will be used to parse the incoming requests. The Pusher Node.js library will be used to publish user_typing
events through the Pusher API.
When done, the dependency section of your package.json file should look like this:
1"dependencies": { 2 "express": "^4.14.1", 3 "body-parser": "^1.16.0", 4 "pusher": "^1.5.1" 5 }
To serve our application we need to do three things:
Create a file and name it server.js
. Inside it, we initialize Express and Pusher like this:
1const express = require('express'); 2const bodyParser = require('body-parser'); 3const path = require('path'); 4const Pusher = require('pusher'); 5 6const app = express(); 7 8//Initialize Pusher 9const pusherConfig = { 10 appId: 'YOUR_PUSHER_APP_ID', 11 key: 'YOUR_PUSHER_KEY', 12 secret: 'YOUR_PUSHER_SECRET', 13 encrypted: true 14}; 15const pusher = new Pusher(pusherConfig); 16 17app.use(bodyParser.urlencoded({extended: true}));
Remember to replace the parameters in the pusherConfig
object with the Pusher credentials you copied earlier from the Pusher dashboard.
Create a route that uses Pusher to broadcast a user_typing
event.
1const chatChannel = 'anonymous_chat'; 2const userIsTypingEvent = 'user_typing'; 3 4app.post('/userTyping', function(req, res) { 5 const username = req.body.username; 6 pusher.trigger(chatChannel, userIsTypingEvent, {username: username}); 7 res.status(200).send(); 8});
This route broadcasts the request's username to everyone who is subscribed to the channel.
Start the Express server to listen on the app port 3000
.
1app.listen(3000, function () { 2 console.log('Node server running on port 3000'); 3});
Now we have the application server set up. Next, we develop the Android application users will interact with.
Open Android Studio and create a new project:
You could name the application whatever suits you, but for the purpose of this tutorial, we will name it 'WhoIsTypingApp'.
On the Next Page, select the API 19: Android 4.4 (Kitkat)
as the Minimum SDK as shown below:
Next, select an 'Empty Activity' as the initial Activity for the Application:
And use the default name of MainActivity with backward compatibility:
Once Android Studio is done with the project's setup, then it's time to install the project dependencies.
In the dependencies section of the build.gradle
file of your application module, add the following:
1dependencies { 2 ... 3 compile 'com.pusher:pusher-java-client:1.4.0' 4 compile 'com.squareup.okhttp3:okhttp:3.3.1' 5 compile 'com.google.code.gson:gson:2.7' 6}
We will be using gson to convert JSON messages to Java Objects. For the network requests to our Node.js Server, we will use okhttp.
Sync the Gradle project so the modules can be installed and the project built.
Next, let's add the INTERNET permission to our AndroidManifest.xml
file. This is required because our application will be connecting to Pusher and our Node.js server over the internet.
1<?xml version="1.0" encoding="utf-8"?> 2<manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 package="com.pusher.whoistypingapp"> 4 5 <uses-permission android:name="android.permission.INTERNET" /> 6 7 <application 8 android:allowBackup="true" 9 android:icon="@mipmap/ic_launcher" 10 android:label="@string/app_name" 11 android:supportsRtl="true" 12 android:theme="@style/AppTheme"> 13 ... 14 </application> 15 16</manifest>
Next, open the activity_main.xml
layout file and modify it to look like this:
1<?xml version="1.0" encoding="utf-8"?> 2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:tools="http://schemas.android.com/tools" 4 android:id="@+id/activity_main" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:paddingBottom="@dimen/activity_vertical_margin" 8 android:paddingLeft="@dimen/activity_horizontal_margin" 9 android:paddingRight="@dimen/activity_horizontal_margin" 10 android:paddingTop="@dimen/activity_vertical_margin" 11 tools:context="com.pusher.whoistypingapp.MainActivity"> 12 13 <LinearLayout 14 android:layout_width="match_parent" 15 android:layout_height="wrap_content" 16 android:layout_alignParentBottom="true" 17 android:orientation="horizontal"> 18 19 <EditText 20 android:id="@+id/messageEditText" 21 android:layout_width="match_parent" 22 android:layout_height="wrap_content" 23 android:hint="Enter Message Here" 24 android:layout_weight="1"/> 25 26 <Button 27 android:id="@+id/sendButton" 28 android:layout_width="match_parent" 29 android:layout_height="wrap_content" 30 android:text="Send" 31 android:layout_weight="4"/> 32 </LinearLayout> 33 34</RelativeLayout>
The layout consists of an EditText where the user can enter a message, and a Button beside it to act as the 'send message' button.
We will need to represent the 'who's typing' message as a Plain Old Java Object so it can be easily deserialized by gson. To do this, create the class com.pusher.whoistypingapp.WhosTyping
and populate it as shown below:
1package com.pusher.whoistypingapp; 2 3public class WhosTyping { 4 public String username; 5 6 public WhosTyping(String username) { 7 this.username = username; 8 } 9}
This WhosTyping
class corresponds to JSON of the following structure:
1{ 2 "username": "Any Name" 3}
Now open the class com.pusher.whoistypingapp.MainActivity
. First, let's start by declaring all the required constants:
1public class MainActivity extends AppCompatActivity { 2 3 private static final String USER_TYPING_ENDPOINT = "https://{NODE_JS_SERVER_ENDPOINT}/userTyping"; 4 private static final String PUSHER_API_KEY = "PUSHER_API_KEY"; 5 private static final String CHANNEL_NAME = "anonymous_chat"; 6 private static final String USER_TYPING_EVENT = "user_typing"; 7 8 ...
Remember to replace the USER_TYPING_ENDPOINT
with the actual hostname (or IP address) of the Node.js server (more on this later) and also the PUSHER_API_KEY
with the Pusher Key you copied earlier from the Pusher dashboard.
Next, we declare the private variables that will be required for MainActivity
to function:
1... 2 Pusher pusher = new Pusher(PUSHER_API_KEY); 3 OkHttpClient httpClient = new OkHttpClient(); 4 5 EditText messageEditText; 6 ...
First, let's implement publishing the user_typing
event to our Node.js server. To do this, we create a TextWatcher
inside the onCreate
method.
1public class MainActivity extends AppCompatActivity { 2 ... 3 @Override 4 protected void onCreate(Bundle savedInstanceState) { 5 super.onCreate(savedInstanceState); 6 setContentView(R.layout.activity_main); 7 8 TextWatcher messageInputTextWatcher = new TextWatcher() { 9 ... 10 @Override 11 public void onTextChanged(CharSequence charSequence, int start, int before, int count) { 12 Log.d("User Input Change", charSequence.toString()); 13 Request userIsTypingRequest = new Request.Builder() 14 .url(USER_TYPING_ENDPOINT) 15 .post(new FormBody.Builder() 16 .add("username", getCurrentUsername()) 17 .build()) 18 .build(); 19 20 httpClient.newCall(userIsTypingRequest) 21 .enqueue(new Callback() { 22 @Override 23 public void onFailure(Call call, IOException e) { 24 Log.d("Post Response", e.toString()); 25 } 26 27 @Override 28 public void onResponse(Call call, Response response) throws IOException { 29 Log.d("Post Response", response.toString()); 30 } 31 }); 32 } 33 ... 34 }; 35 36 ...
Inside the onTextChanged
method of the TextWatcher, we build the userIsTypingRequest
and then send the request to the USER_TYPING_ENDPOINT
URL. For simplicity, we just log the response we get for the server.
Then we add the text change listener to the messageEditText
as shown below.
1... 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 ... 5 messageEditText = (EditText)findViewById(R.id.messageEditText); 6 messageEditText.addTextChangedListener(messageInputTextWatcher); 7 ... 8 }
Now, whenever a user starts typing, a request is sent to the server and the server will in turn broadcast the typing event to all other users.
Next, we need to subscribe to the user_typing
event.
We create a SubscriptionEventListener
that will respond when a user_typing
event arrives:
1... 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 ... 5 6 SubscriptionEventListener isTypingEventListener = new SubscriptionEventListener() { 7 @Override 8 public void onEvent(String channel, String event, String data) { 9 final WhosTyping whosTyping = new Gson().fromJson(data, WhosTyping.class); 10 if(!whosTyping.username.equals(getCurrentUsername())) { 11 runOnUiThread(new Runnable() { 12 @Override 13 public void run() { 14 getSupportActionBar().setSubtitle(whosTyping.username + " is typing..."); 15 } 16 }); 17 } 18 } 19 }; 20 21 ... 22 }
Here, the JSON string we receive is converted to a WhosTyping
object using gson. Then we check if the username of the WhosTyping
object is equal to the current username before we update the UI. The typing indicator message is shown as subtitle text on the Action Bar.
Then we subscribe and bind the isTypingEventListener
to the user_typing
event:
1... 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 ... 5 6 Channel pusherChannel = pusher.subscribe(CHANNEL_NAME); 7 pusherChannel.bind(USER_TYPING_EVENT, isTypingEventListener); 8 }
The application now updates the UI with the username of 'who's typing'. But the typing indicator message needs to be cleared when the user stops typing or else the message stays forever. An easy solution is to set a timer that clears the typing message after some seconds of not receiving an event. From experience, a clear timer of 0.9 seconds has given the best results.
To set the clear timer, we use the java.util.Timer
and java.util.TimerTask
classes. First, let us create a method that starts the clear timer:
1public class MainActivity extends AppCompatActivity { 2 ... 3 4 TimerTask clearTimerTask; 5 Timer clearTimer; 6 7 private void startClearTimer() { 8 clearTimerTask = new TimerTask() { 9 @Override 10 public void run() { 11 runOnUiThread(new Runnable() { 12 @Override 13 public void run() { 14 getSupportActionBar().setSubtitle(""); 15 } 16 }); 17 } 18 }; 19 clearTimer = new Timer(); 20 long interval = 900; //0.9 seconds 21 clearTimer.schedule(clearTimerTask, interval); 22 } 23 ...
The clearTimerTask
will clear the Action Bar's subtitle when it is invoked by the clearTimer
after 0.9 seconds.
Next, we update the onEvent
method of our SubscriptionEventListener
to start the clear timer.
1... 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 ... 5 6 SubscriptionEventListener isTypingEventListener = new SubscriptionEventListener() { 7 @Override 8 public void onEvent(String channel, String event, String data) { 9 ... 10 11 //reset timer 12 if(clearTimer != null) { 13 clearTimer.cancel(); 14 } 15 startClearTimer(); 16 } 17 }; 18 19 ... 20 }
And there you have it. The chat application now has the functionality to display who's currently typing.
Finally, override the onResume()
and onPause()
methods of MainActivity
to connect and disconnect the pusher
object respectively.
1public class MainActivity extends AppCompatActivity { 2 ... 3 4 @Override 5 protected void onResume() { 6 super.onResume(); 7 pusher.connect(); 8 } 9 10 @Override 11 protected void onPause() { 12 pusher.disconnect(); 13 super.onPause(); 14 } 15 16 }
First, ensure you have updated your the PUSHER_API_KEY
in the MainActivity
class with your Pusher Key.
Run the Android application either using a real device or a virtual one. You should see an interface like this:
The easiest way to test the Android application is through the Pusher Debug Console on your Dashboard. At the Debug Console for your app on Pusher, click to show the event creator and then fill the Channel, Event and Data field as shown in the image below:
When you click the ‘Send event’ button, the interface of your Android application will update to indicate the ‘username is typing…’ message at the top of the page as shown in the image below:
To test the application with the Node.js server, you will need to make the server available to the Android application either by hosting it live or maybe using a tunneling tool like ngrok.
Then update the USER_TYPING_ENDPOINT
constant in the MainActivity
class with the server's URL. Now to test, you need to run the Android application on two devices. When you start typing in one, you should notice the other device shows that you are currently typing.
In this tutorial, we saw how to build a typing indicator in an Android app using Pusher.