Build a typing indicator in Android

Introduction

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.

Overview of the chat app

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.

Setup a Pusher account

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:

typing-indicator-android-create-app

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.

Setup the Node.js server

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:

  • Set up Express and Pusher.
  • Create routes to listen for web requests.
  • Start the Express server.

Setup Express and Pusher

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 routes to serve our application

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

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.

Set up the Android project

Open Android Studio and create a new project:

typing-indicator-android-create-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:

typing-indicator-android-android-4-4

Next, select an 'Empty Activity' as the initial Activity for the Application:

typing-indicator-android-add-activity

And use the default name of MainActivity with backward compatibility:

typing-indicator-android-customize-activity

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>

The chat activity layout

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.

The typing indicator model

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}

The chat activity

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    ...

Publishing

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.

Subscribing to

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 }

Testing

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:

typing-indicator-android-launch-view

Testing with the debug console

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:

typing-indicator-android-debug-console

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:

typing-indicator-android-demo

Testing with the Node.js server

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.

Conclusion

In this tutorial, we saw how to build a typing indicator in an Android app using Pusher.