Skip to contentSkip to navigationSkip to topbar
On this page

Chat with Android and Java


(warning)

Warning

As the Programmable Chat API is set to sunset in 2022(link takes you to an external page), we will no longer maintain these chat tutorials.

Please see our Conversations API QuickStart to start building robust virtual spaces for conversation.

(error)

Danger

Programmable Chat has been deprecated and is no longer supported. Instead, we'll be focusing on the next generation of chat: Twilio Conversations. Find out more about the EOL process here(link takes you to an external page).

If you're starting a new project, please visit the Conversations Docs to begin. If you've already built on Programmable Chat, please visit our Migration Guide to learn about how to switch.

Ready to implement a chat application using Twilio Programmable Chat Client? Here is how it works at a high level:

  1. Twilio Programmable Chat(link takes you to an external page) is the core product we'll be using to handle all the chat functionality
  2. We use a server side app to generate a user access token which contains all your Twilio account information. The Programmable chat Client uses this token to connect with the API

Properati built a web and mobile messaging app to help real estate buyers and sellers connect in real time. Learn more here.(link takes you to an external page)

For your convenience, we consolidated the source code for this tutorial in a single GitHub repository(link takes you to an external page). Feel free to clone it and tweak it as required.


Initialize the Client - Part 1: Fetch access token

initialize-the-client---part-1-fetch-access-token page anchor

The first thing you need to create a client is an access token. This token holds information about your Twilio account and Programmable Chat API keys. We have created a web version of Twilio chat in different languages. You can use any of these to generate the token:

We use Volley(link takes you to an external page) to make a request to our server and get the access token.

Fetch Access Token

fetch-access-token page anchor

app/src/main/java/com/twilio/twiliochat/chat/accesstoken/AccessTokenFetcher.java

1
package com.twilio.twiliochat.chat.accesstoken;
2
3
import android.content.Context;
4
import android.content.res.Resources;
5
import android.util.Log;
6
7
import com.android.volley.Request;
8
import com.android.volley.Response;
9
import com.android.volley.VolleyError;
10
import com.android.volley.toolbox.JsonObjectRequest;
11
import com.twilio.twiliochat.R;
12
import com.twilio.twiliochat.application.SessionManager;
13
import com.twilio.twiliochat.application.TwilioChatApplication;
14
import com.twilio.twiliochat.chat.listeners.TaskCompletionListener;
15
16
import org.json.JSONException;
17
import org.json.JSONObject;
18
19
import java.util.HashMap;
20
import java.util.Map;
21
22
public class AccessTokenFetcher {
23
24
private Context context;
25
26
public AccessTokenFetcher(Context context) {
27
this.context = context;
28
}
29
30
public void fetch(final TaskCompletionListener<String, String> listener) {
31
JSONObject obj = new JSONObject(getTokenRequestParams(context));
32
String identity = SessionManager.getInstance().getUsername();
33
String requestUrl = getStringResource(R.string.token_url) + "?identity=" + identity;
34
Log.d(TwilioChatApplication.TAG, "Requesting access token from: " + requestUrl);
35
36
JsonObjectRequest jsonObjReq =
37
new JsonObjectRequest(Request.Method.GET, requestUrl, obj, new Response.Listener<JSONObject>() {
38
39
@Override
40
public void onResponse(JSONObject response) {
41
String token = null;
42
try {
43
token = response.getString("token");
44
} catch (JSONException e) {
45
Log.e(TwilioChatApplication.TAG, e.getLocalizedMessage(), e);
46
listener.onError("Failed to parse token JSON response");
47
}
48
listener.onSuccess(token);
49
}
50
}, new Response.ErrorListener() {
51
52
@Override
53
public void onErrorResponse(VolleyError error) {
54
Log.e(TwilioChatApplication.TAG, error.getLocalizedMessage(), error);
55
listener.onError("Failed to fetch token");
56
}
57
});
58
jsonObjReq.setShouldCache(false);
59
TokenRequest.getInstance().addToRequestQueue(jsonObjReq);
60
}
61
62
private Map<String, String> getTokenRequestParams(Context context) {
63
Map<String, String> params = new HashMap<>();
64
params.put("identity", SessionManager.getInstance().getUsername());
65
return params;
66
}
67
68
private String getStringResource(int id) {
69
Resources resources = TwilioChatApplication.get().getResources();
70
return resources.getString(id);
71
}
72
73
}
74

Now it's time to use that token to initialize your Twilio client.


Initialize the Client - Part 2: Build the client

initialize-the-client---part-2-build-the-client page anchor

Before creating a Programmable Chat Client instance we need to decide the region it will connect to and the synchronization strategy used to initialize it.

We can then pass this information along with the access token generated in the previous step and wait for the client to be ready.

app/src/main/java/com/twilio/twiliochat/chat/ChatClientBuilder.java

1
package com.twilio.twiliochat.chat;
2
3
import android.content.Context;
4
5
import com.twilio.chat.CallbackListener;
6
import com.twilio.chat.ChatClient;
7
import com.twilio.chat.ErrorInfo;
8
import com.twilio.twiliochat.chat.listeners.TaskCompletionListener;
9
10
public class ChatClientBuilder extends CallbackListener<ChatClient> {
11
12
private Context context;
13
private TaskCompletionListener<ChatClient, String> buildListener;
14
15
public ChatClientBuilder(Context context) {
16
this.context = context;
17
}
18
19
public void build(String token, final TaskCompletionListener<ChatClient, String> listener) {
20
ChatClient.Properties props =
21
new ChatClient.Properties.Builder()
22
.setRegion("us1")
23
.createProperties();
24
25
this.buildListener = listener;
26
ChatClient.create(context.getApplicationContext(),
27
token,
28
props,
29
this);
30
}
31
32
33
@Override
34
public void onSuccess(ChatClient chatClient) {
35
this.buildListener.onSuccess(chatClient);
36
}
37
38
@Override
39
public void onError(ErrorInfo errorInfo) {
40
this.buildListener.onError(errorInfo.getMessage());
41
}
42
}
43

The next step will be getting a channel list.


Our ChannelManager class takes care of everything related to channels. The first thing we need to do when the class is initialized, is to store a list of channels of type Channel. To do this we call the method getChannels from the Programmable Chat Client and extract a Channel object from each ChannelDescriptor returned.

app/src/main/java/com/twilio/twiliochat/chat/channels/ChannelManager.java

1
package com.twilio.twiliochat.chat.channels;
2
3
import android.content.res.Resources;
4
import android.os.Handler;
5
import android.os.Looper;
6
import android.util.Log;
7
8
import com.twilio.chat.CallbackListener;
9
import com.twilio.chat.Channel;
10
import com.twilio.chat.Channel.ChannelType;
11
import com.twilio.chat.ChannelDescriptor;
12
import com.twilio.chat.Channels;
13
import com.twilio.chat.ChatClient;
14
import com.twilio.chat.ChatClientListener;
15
import com.twilio.chat.ErrorInfo;
16
import com.twilio.chat.Paginator;
17
import com.twilio.chat.StatusListener;
18
import com.twilio.chat.User;
19
import com.twilio.twiliochat.R;
20
import com.twilio.twiliochat.application.TwilioChatApplication;
21
import com.twilio.twiliochat.chat.ChatClientManager;
22
import com.twilio.twiliochat.chat.accesstoken.AccessTokenFetcher;
23
import com.twilio.twiliochat.chat.listeners.TaskCompletionListener;
24
25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.List;
28
29
public class ChannelManager implements ChatClientListener {
30
private static ChannelManager sharedManager = new ChannelManager();
31
public Channel generalChannel;
32
private ChatClientManager chatClientManager;
33
private ChannelExtractor channelExtractor;
34
private List<Channel> channels;
35
private Channels channelsObject;
36
private ChatClientListener listener;
37
private String defaultChannelName;
38
private String defaultChannelUniqueName;
39
private Handler handler;
40
private Boolean isRefreshingChannels = false;
41
42
private ChannelManager() {
43
this.chatClientManager = TwilioChatApplication.get().getChatClientManager();
44
this.channelExtractor = new ChannelExtractor();
45
this.listener = this;
46
defaultChannelName = getStringResource(R.string.default_channel_name);
47
defaultChannelUniqueName = getStringResource(R.string.default_channel_unique_name);
48
handler = setupListenerHandler();
49
}
50
51
public static ChannelManager getInstance() {
52
return sharedManager;
53
}
54
55
public List<Channel> getChannels() {
56
return channels;
57
}
58
59
public String getDefaultChannelName() {
60
return this.defaultChannelName;
61
}
62
63
public void leaveChannelWithHandler(Channel channel, StatusListener handler) {
64
channel.leave(handler);
65
}
66
67
public void deleteChannelWithHandler(Channel channel, StatusListener handler) {
68
channel.destroy(handler);
69
}
70
71
public void populateChannels(final LoadChannelListener listener) {
72
if (this.chatClientManager == null || this.isRefreshingChannels) {
73
return;
74
}
75
this.isRefreshingChannels = true;
76
77
handler.post(new Runnable() {
78
@Override
79
public void run() {
80
channelsObject = chatClientManager.getChatClient().getChannels();
81
82
channelsObject.getPublicChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
83
@Override
84
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
85
extractChannelsFromPaginatorAndPopulate(channelDescriptorPaginator, listener);
86
}
87
});
88
89
}
90
});
91
}
92
93
private void extractChannelsFromPaginatorAndPopulate(final Paginator<ChannelDescriptor> channelsPaginator,
94
final LoadChannelListener listener) {
95
channels = new ArrayList<>();
96
ChannelManager.this.channels.clear();
97
channelExtractor.extractAndSortFromChannelDescriptor(channelsPaginator,
98
new TaskCompletionListener<List<Channel>, String>() {
99
@Override
100
public void onSuccess(List<Channel> channels) {
101
ChannelManager.this.channels.addAll(channels);
102
Collections.sort(ChannelManager.this.channels, new CustomChannelComparator());
103
ChannelManager.this.isRefreshingChannels = false;
104
chatClientManager.addClientListener(ChannelManager.this);
105
listener.onChannelsFinishedLoading(ChannelManager.this.channels);
106
}
107
108
@Override
109
public void onError(String errorText) {
110
Log.e(TwilioChatApplication.TAG,"Error populating channels: " + errorText);
111
}
112
});
113
}
114
115
public void createChannelWithName(String name, final StatusListener handler) {
116
this.channelsObject
117
.channelBuilder()
118
.withFriendlyName(name)
119
.withType(ChannelType.PUBLIC)
120
.build(new CallbackListener<Channel>() {
121
@Override
122
public void onSuccess(final Channel newChannel) {
123
handler.onSuccess();
124
}
125
126
@Override
127
public void onError(ErrorInfo errorInfo) {
128
handler.onError(errorInfo);
129
}
130
});
131
}
132
133
public void joinOrCreateGeneralChannelWithCompletion(final StatusListener listener) {
134
channelsObject.getChannel(defaultChannelUniqueName, new CallbackListener<Channel>() {
135
@Override
136
public void onSuccess(Channel channel) {
137
ChannelManager.this.generalChannel = channel;
138
if (channel != null) {
139
joinGeneralChannelWithCompletion(listener);
140
} else {
141
createGeneralChannelWithCompletion(listener);
142
}
143
}
144
});
145
}
146
147
private void joinGeneralChannelWithCompletion(final StatusListener listener) {
148
if (generalChannel.getStatus() == Channel.ChannelStatus.JOINED) {
149
listener.onSuccess();
150
return;
151
}
152
this.generalChannel.join(new StatusListener() {
153
@Override
154
public void onSuccess() {
155
listener.onSuccess();
156
}
157
158
@Override
159
public void onError(ErrorInfo errorInfo) {
160
listener.onError(errorInfo);
161
}
162
});
163
}
164
165
private void createGeneralChannelWithCompletion(final StatusListener listener) {
166
this.channelsObject
167
.channelBuilder()
168
.withFriendlyName(defaultChannelName)
169
.withUniqueName(defaultChannelUniqueName)
170
.withType(ChannelType.PUBLIC)
171
.build(new CallbackListener<Channel>() {
172
@Override
173
public void onSuccess(final Channel channel) {
174
ChannelManager.this.generalChannel = channel;
175
ChannelManager.this.channels.add(channel);
176
joinGeneralChannelWithCompletion(listener);
177
}
178
179
@Override
180
public void onError(ErrorInfo errorInfo) {
181
listener.onError(errorInfo);
182
}
183
});
184
}
185
186
public void setChannelListener(ChatClientListener listener) {
187
this.listener = listener;
188
}
189
190
private String getStringResource(int id) {
191
Resources resources = TwilioChatApplication.get().getResources();
192
return resources.getString(id);
193
}
194
195
@Override
196
public void onChannelAdded(Channel channel) {
197
if (listener != null) {
198
listener.onChannelAdded(channel);
199
}
200
}
201
202
@Override
203
public void onChannelUpdated(Channel channel, Channel.UpdateReason updateReason) {
204
if (listener != null) {
205
listener.onChannelUpdated(channel, updateReason);
206
}
207
}
208
209
@Override
210
public void onChannelDeleted(Channel channel) {
211
if (listener != null) {
212
listener.onChannelDeleted(channel);
213
}
214
}
215
216
@Override
217
public void onChannelSynchronizationChange(Channel channel) {
218
if (listener != null) {
219
listener.onChannelSynchronizationChange(channel);
220
}
221
}
222
223
@Override
224
public void onError(ErrorInfo errorInfo) {
225
if (listener != null) {
226
listener.onError(errorInfo);
227
}
228
}
229
230
@Override
231
public void onClientSynchronization(ChatClient.SynchronizationStatus synchronizationStatus) {
232
233
}
234
235
@Override
236
public void onChannelJoined(Channel channel) {
237
238
}
239
240
@Override
241
public void onChannelInvited(Channel channel) {
242
243
}
244
245
@Override
246
public void onUserUpdated(User user, User.UpdateReason updateReason) {
247
if (listener != null) {
248
listener.onUserUpdated(user, updateReason);
249
}
250
}
251
252
@Override
253
public void onUserSubscribed(User user) {
254
255
}
256
257
@Override
258
public void onUserUnsubscribed(User user) {
259
260
}
261
262
@Override
263
public void onNewMessageNotification(String s, String s1, long l) {
264
265
}
266
267
@Override
268
public void onAddedToChannelNotification(String s) {
269
270
}
271
272
@Override
273
public void onInvitedToChannelNotification(String s) {
274
275
}
276
277
@Override
278
public void onRemovedFromChannelNotification(String s) {
279
280
}
281
282
@Override
283
public void onNotificationSubscribed() {
284
285
}
286
287
@Override
288
public void onNotificationFailed(ErrorInfo errorInfo) {
289
290
}
291
292
@Override
293
public void onConnectionStateChange(ChatClient.ConnectionState connectionState) {
294
295
}
296
297
@Override
298
public void onTokenExpired() {
299
refreshAccessToken();
300
}
301
302
@Override
303
public void onTokenAboutToExpire() {
304
refreshAccessToken();
305
}
306
307
private void refreshAccessToken() {
308
AccessTokenFetcher accessTokenFetcher = chatClientManager.getAccessTokenFetcher();
309
accessTokenFetcher.fetch(new TaskCompletionListener<String, String>() {
310
@Override
311
public void onSuccess(String token) {
312
ChannelManager.this.chatClientManager.getChatClient().updateToken(token, new StatusListener() {
313
@Override
314
public void onSuccess() {
315
Log.d(TwilioChatApplication.TAG, "Successfully updated access token.");
316
}
317
});
318
}
319
320
@Override
321
public void onError(String message) {
322
Log.e(TwilioChatApplication.TAG,"Error trying to fetch token: " + message);
323
}
324
});
325
}
326
327
private Handler setupListenerHandler() {
328
Looper looper;
329
Handler handler;
330
if ((looper = Looper.myLooper()) != null) {
331
handler = new Handler(looper);
332
} else if ((looper = Looper.getMainLooper()) != null) {
333
handler = new Handler(looper);
334
} else {
335
throw new IllegalArgumentException("Channel Listener must have a Looper.");
336
}
337
return handler;
338
}
339
}
340

Let's see how we can listen to events from the chat client so we can update our app's state.


The Programmable Chat Client will trigger events such as onChannelAdded or onChannelDeleted on our application. Given the creation or deletion of a channel, we'll reload the channel list in the sliding panel. If a channel is deleted, but we were currently on that same channel, the application will automatically join the general channel.

You must set your ChatClient to listen to events(link takes you to an external page) using a ChatClientListener(link takes you to an external page). In this particular case, MainChatActivity implements ChatClientListener, but it's methods are called from the ChannelManager class that also implements ChatClientListener (who is the client's listener). ChannelManager is used as an event handler proxy. Twilio chat sets the listener when loading the channels.

Listen for Client Events

listen-for-client-events page anchor

app/src/main/java/com/twilio/twiliochat/chat/MainChatActivity.java

1
package com.twilio.twiliochat.chat;
2
3
import android.app.Activity;
4
import android.app.ProgressDialog;
5
import android.content.Context;
6
import android.content.DialogInterface;
7
import android.content.Intent;
8
import android.content.res.Resources;
9
import android.os.Bundle;
10
import android.os.Handler;
11
import android.util.Log;
12
import android.view.Menu;
13
import android.view.MenuItem;
14
import android.view.View;
15
import android.widget.AdapterView;
16
import android.widget.Button;
17
import android.widget.ListView;
18
import android.widget.TextView;
19
20
import androidx.appcompat.app.ActionBarDrawerToggle;
21
import androidx.appcompat.app.AppCompatActivity;
22
import androidx.appcompat.widget.Toolbar;
23
import androidx.core.view.GravityCompat;
24
import androidx.drawerlayout.widget.DrawerLayout;
25
import androidx.fragment.app.FragmentTransaction;
26
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
27
28
import com.twilio.chat.Channel;
29
import com.twilio.chat.ChatClient;
30
import com.twilio.chat.ChatClientListener;
31
import com.twilio.chat.ErrorInfo;
32
import com.twilio.chat.StatusListener;
33
import com.twilio.chat.User;
34
import com.twilio.twiliochat.R;
35
import com.twilio.twiliochat.application.AlertDialogHandler;
36
import com.twilio.twiliochat.application.SessionManager;
37
import com.twilio.twiliochat.application.TwilioChatApplication;
38
import com.twilio.twiliochat.chat.channels.ChannelAdapter;
39
import com.twilio.twiliochat.chat.channels.ChannelManager;
40
import com.twilio.twiliochat.chat.channels.LoadChannelListener;
41
import com.twilio.twiliochat.chat.listeners.InputOnClickListener;
42
import com.twilio.twiliochat.chat.listeners.TaskCompletionListener;
43
import com.twilio.twiliochat.landing.LoginActivity;
44
45
import java.util.List;
46
47
public class MainChatActivity extends AppCompatActivity implements ChatClientListener {
48
private Context context;
49
private Activity mainActivity;
50
private Button logoutButton;
51
private Button addChannelButton;
52
private TextView usernameTextView;
53
private ChatClientManager chatClientManager;
54
private ListView channelsListView;
55
private ChannelAdapter channelAdapter;
56
private ChannelManager channelManager;
57
private MainChatFragment chatFragment;
58
private DrawerLayout drawer;
59
private ProgressDialog progressDialog;
60
private MenuItem leaveChannelMenuItem;
61
private MenuItem deleteChannelMenuItem;
62
private SwipeRefreshLayout refreshLayout;
63
64
@Override
65
protected void onDestroy() {
66
super.onDestroy();
67
new Handler().post(new Runnable() {
68
@Override
69
public void run() {
70
chatClientManager.shutdown();
71
TwilioChatApplication.get().getChatClientManager().setChatClient(null);
72
}
73
});
74
}
75
76
@Override
77
protected void onCreate(Bundle savedInstanceState) {
78
super.onCreate(savedInstanceState);
79
setContentView(R.layout.activity_main_chat);
80
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
81
setSupportActionBar(toolbar);
82
83
drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
84
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar,
85
R.string.navigation_drawer_open, R.string.navigation_drawer_close);
86
drawer.setDrawerListener(toggle);
87
toggle.syncState();
88
89
refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refreshLayout);
90
91
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
92
93
chatFragment = new MainChatFragment();
94
fragmentTransaction.add(R.id.fragment_container, chatFragment);
95
fragmentTransaction.commit();
96
97
context = this;
98
mainActivity = this;
99
logoutButton = (Button) findViewById(R.id.buttonLogout);
100
addChannelButton = (Button) findViewById(R.id.buttonAddChannel);
101
usernameTextView = (TextView) findViewById(R.id.textViewUsername);
102
channelsListView = (ListView) findViewById(R.id.listViewChannels);
103
104
channelManager = ChannelManager.getInstance();
105
setUsernameTextView();
106
107
setUpListeners();
108
checkTwilioClient();
109
}
110
111
private void setUpListeners() {
112
logoutButton.setOnClickListener(new View.OnClickListener() {
113
@Override
114
public void onClick(View v) {
115
promptLogout();
116
}
117
});
118
addChannelButton.setOnClickListener(new View.OnClickListener() {
119
@Override
120
public void onClick(View v) {
121
showAddChannelDialog();
122
}
123
});
124
refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
125
@Override
126
public void onRefresh() {
127
refreshLayout.setRefreshing(true);
128
refreshChannels();
129
}
130
});
131
channelsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
132
@Override
133
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
134
setChannel(position);
135
}
136
});
137
}
138
139
@Override
140
public void onBackPressed() {
141
if (drawer.isDrawerOpen(GravityCompat.START)) {
142
drawer.closeDrawer(GravityCompat.START);
143
} else {
144
super.onBackPressed();
145
}
146
}
147
148
@Override
149
public boolean onCreateOptionsMenu(Menu menu) {
150
getMenuInflater().inflate(R.menu.main_chat, menu);
151
this.leaveChannelMenuItem = menu.findItem(R.id.action_leave_channel);
152
this.leaveChannelMenuItem.setVisible(false);
153
this.deleteChannelMenuItem = menu.findItem(R.id.action_delete_channel);
154
this.deleteChannelMenuItem.setVisible(false);
155
return true;
156
}
157
158
@Override
159
public boolean onOptionsItemSelected(MenuItem item) {
160
int id = item.getItemId();
161
162
if (id == R.id.action_leave_channel) {
163
leaveCurrentChannel();
164
return true;
165
}
166
if (id == R.id.action_delete_channel) {
167
promptChannelDeletion();
168
}
169
170
return super.onOptionsItemSelected(item);
171
}
172
173
private String getStringResource(int id) {
174
Resources resources = getResources();
175
return resources.getString(id);
176
}
177
178
private void refreshChannels() {
179
channelManager.populateChannels(new LoadChannelListener() {
180
@Override
181
public void onChannelsFinishedLoading(final List<Channel> channels) {
182
runOnUiThread(new Runnable() {
183
@Override
184
public void run() {
185
channelAdapter.setChannels(channels);
186
refreshLayout.setRefreshing(false);
187
}
188
});
189
}
190
});
191
}
192
193
private void populateChannels() {
194
channelManager.setChannelListener(this);
195
channelManager.populateChannels(new LoadChannelListener() {
196
@Override
197
public void onChannelsFinishedLoading(List<Channel> channels) {
198
channelAdapter = new ChannelAdapter(mainActivity, channels);
199
channelsListView.setAdapter(channelAdapter);
200
MainChatActivity.this.channelManager
201
.joinOrCreateGeneralChannelWithCompletion(new StatusListener() {
202
@Override
203
public void onSuccess() {
204
runOnUiThread(new Runnable() {
205
@Override
206
public void run() {
207
channelAdapter.notifyDataSetChanged();
208
stopActivityIndicator();
209
setChannel(0);
210
}
211
});
212
}
213
214
@Override
215
public void onError(ErrorInfo errorInfo) {
216
showAlertWithMessage(getStringResource(R.string.generic_error) + " - " + errorInfo.getMessage());
217
}
218
});
219
}
220
});
221
}
222
223
private void setChannel(final int position) {
224
List<Channel> channels = channelManager.getChannels();
225
if (channels == null) {
226
return;
227
}
228
final Channel currentChannel = chatFragment.getCurrentChannel();
229
final Channel selectedChannel = channels.get(position);
230
if (currentChannel != null && currentChannel.getSid().contentEquals(selectedChannel.getSid())) {
231
drawer.closeDrawer(GravityCompat.START);
232
return;
233
}
234
hideMenuItems(position);
235
if (selectedChannel != null) {
236
showActivityIndicator("Joining " + selectedChannel.getFriendlyName() + " channel");
237
if (currentChannel != null && currentChannel.getStatus() == Channel.ChannelStatus.JOINED) {
238
this.channelManager.leaveChannelWithHandler(currentChannel, new StatusListener() {
239
@Override
240
public void onSuccess() {
241
joinChannel(selectedChannel);
242
}
243
244
@Override
245
public void onError(ErrorInfo errorInfo) {
246
stopActivityIndicator();
247
}
248
});
249
return;
250
}
251
joinChannel(selectedChannel);
252
stopActivityIndicator();
253
} else {
254
stopActivityIndicator();
255
showAlertWithMessage(getStringResource(R.string.generic_error));
256
Log.e(TwilioChatApplication.TAG,"Selected channel out of range");
257
}
258
}
259
260
private void joinChannel(final Channel selectedChannel) {
261
runOnUiThread(new Runnable() {
262
@Override
263
public void run() {
264
chatFragment.setCurrentChannel(selectedChannel, new StatusListener() {
265
@Override
266
public void onSuccess() {
267
MainChatActivity.this.stopActivityIndicator();
268
}
269
270
@Override
271
public void onError(ErrorInfo errorInfo) {
272
}
273
});
274
setTitle(selectedChannel.getFriendlyName());
275
drawer.closeDrawer(GravityCompat.START);
276
}
277
});
278
}
279
280
private void hideMenuItems(final int position) {
281
runOnUiThread(new Runnable() {
282
@Override
283
public void run() {
284
MainChatActivity.this.leaveChannelMenuItem.setVisible(position != 0);
285
MainChatActivity.this.deleteChannelMenuItem.setVisible(position != 0);
286
}
287
});
288
}
289
290
private void showAddChannelDialog() {
291
String message = getStringResource(R.string.new_channel_prompt);
292
AlertDialogHandler.displayInputDialog(message, context, new InputOnClickListener() {
293
@Override
294
public void onClick(String input) {
295
if (input.length() == 0) {
296
showAlertWithMessage(getStringResource(R.string.channel_name_required_message));
297
return;
298
}
299
createChannelWithName(input);
300
}
301
});
302
}
303
304
private void createChannelWithName(String name) {
305
name = name.trim();
306
if (name.toLowerCase()
307
.contentEquals(this.channelManager.getDefaultChannelName().toLowerCase())) {
308
showAlertWithMessage(getStringResource(R.string.channel_name_equals_default_name));
309
return;
310
}
311
this.channelManager.createChannelWithName(name, new StatusListener() {
312
@Override
313
public void onSuccess() {
314
refreshChannels();
315
}
316
317
@Override
318
public void onError(ErrorInfo errorInfo) {
319
showAlertWithMessage(getStringResource(R.string.generic_error) + " - " + errorInfo.getMessage());
320
}
321
});
322
}
323
324
private void promptChannelDeletion() {
325
String message = getStringResource(R.string.channel_delete_prompt_message);
326
AlertDialogHandler.displayCancellableAlertWithHandler(message, context,
327
new DialogInterface.OnClickListener() {
328
@Override
329
public void onClick(DialogInterface dialog, int which) {
330
deleteCurrentChannel();
331
}
332
});
333
}
334
335
private void deleteCurrentChannel() {
336
Channel currentChannel = chatFragment.getCurrentChannel();
337
channelManager.deleteChannelWithHandler(currentChannel, new StatusListener() {
338
@Override
339
public void onSuccess() {
340
}
341
342
@Override
343
public void onError(ErrorInfo errorInfo) {
344
showAlertWithMessage(getStringResource(R.string.message_deletion_forbidden));
345
}
346
});
347
}
348
349
private void leaveCurrentChannel() {
350
final Channel currentChannel = chatFragment.getCurrentChannel();
351
if (currentChannel.getStatus() == Channel.ChannelStatus.NOT_PARTICIPATING) {
352
setChannel(0);
353
return;
354
}
355
channelManager.leaveChannelWithHandler(currentChannel, new StatusListener() {
356
@Override
357
public void onSuccess() {
358
setChannel(0);
359
}
360
361
@Override
362
public void onError(ErrorInfo errorInfo) {
363
stopActivityIndicator();
364
}
365
});
366
}
367
368
private void checkTwilioClient() {
369
showActivityIndicator(getStringResource(R.string.loading_channels_message));
370
chatClientManager = TwilioChatApplication.get().getChatClientManager();
371
if (chatClientManager.getChatClient() == null) {
372
initializeClient();
373
} else {
374
populateChannels();
375
}
376
}
377
378
private void initializeClient() {
379
chatClientManager.connectClient(new TaskCompletionListener<Void, String>() {
380
@Override
381
public void onSuccess(Void aVoid) {
382
populateChannels();
383
}
384
385
@Override
386
public void onError(String errorMessage) {
387
stopActivityIndicator();
388
showAlertWithMessage("Client connection error: " + errorMessage);
389
}
390
});
391
}
392
393
private void promptLogout() {
394
final String message = getStringResource(R.string.logout_prompt_message);
395
runOnUiThread(new Runnable() {
396
@Override
397
public void run() {
398
AlertDialogHandler.displayCancellableAlertWithHandler(message, context,
399
new DialogInterface.OnClickListener() {
400
@Override
401
public void onClick(DialogInterface dialog, int which) {
402
SessionManager.getInstance().logoutUser();
403
showLoginActivity();
404
}
405
});
406
}
407
});
408
409
}
410
411
private void showLoginActivity() {
412
Intent launchIntent = new Intent();
413
launchIntent.setClass(getApplicationContext(), LoginActivity.class);
414
startActivity(launchIntent);
415
finish();
416
}
417
418
private void setUsernameTextView() {
419
String username =
420
SessionManager.getInstance().getUserDetails().get(SessionManager.KEY_USERNAME);
421
usernameTextView.setText(username);
422
}
423
424
private void stopActivityIndicator() {
425
runOnUiThread(new Runnable() {
426
@Override
427
public void run() {
428
if (progressDialog.isShowing()) {
429
progressDialog.dismiss();
430
}
431
}
432
});
433
}
434
435
private void showActivityIndicator(final String message) {
436
runOnUiThread(new Runnable() {
437
@Override
438
public void run() {
439
progressDialog = new ProgressDialog(MainChatActivity.this.mainActivity);
440
progressDialog.setMessage(message);
441
progressDialog.show();
442
progressDialog.setCanceledOnTouchOutside(false);
443
progressDialog.setCancelable(false);
444
}
445
});
446
}
447
448
private void showAlertWithMessage(final String message) {
449
runOnUiThread(new Runnable() {
450
@Override
451
public void run() {
452
AlertDialogHandler.displayAlertWithMessage(message, context);
453
}
454
});
455
}
456
457
458
459
@Override
460
public void onChannelAdded(Channel channel) {
461
Log.d(TwilioChatApplication.TAG,"Channel Added");
462
refreshChannels();
463
}
464
465
@Override
466
public void onChannelDeleted(final Channel channel) {
467
Log.d(TwilioChatApplication.TAG,"Channel Deleted");
468
Channel currentChannel = chatFragment.getCurrentChannel();
469
if (channel.getSid().contentEquals(currentChannel.getSid())) {
470
chatFragment.setCurrentChannel(null, null);
471
setChannel(0);
472
}
473
refreshChannels();
474
}
475
476
@Override
477
public void onChannelInvited(Channel channel) {
478
479
}
480
481
@Override
482
public void onChannelSynchronizationChange(Channel channel) {
483
484
}
485
486
@Override
487
public void onError(ErrorInfo errorInfo) {
488
489
}
490
491
@Override
492
public void onClientSynchronization(ChatClient.SynchronizationStatus synchronizationStatus) {
493
494
}
495
496
@Override
497
public void onConnectionStateChange(ChatClient.ConnectionState connectionState) {
498
499
}
500
501
@Override
502
public void onTokenExpired() {
503
504
}
505
506
@Override
507
public void onTokenAboutToExpire() {
508
509
}
510
511
@Override
512
public void onChannelJoined(Channel channel) {
513
514
}
515
516
@Override
517
public void onChannelUpdated(Channel channel, Channel.UpdateReason updateReason) {
518
519
}
520
521
@Override
522
public void onUserUpdated(User user, User.UpdateReason updateReason) {
523
524
}
525
526
@Override
527
public void onUserSubscribed(User user) {
528
529
}
530
531
@Override
532
public void onUserUnsubscribed(User user) {
533
534
}
535
536
@Override
537
public void onNewMessageNotification(String s, String s1, long l) {
538
539
}
540
541
@Override
542
public void onAddedToChannelNotification(String s) {
543
544
}
545
546
@Override
547
public void onInvitedToChannelNotification(String s) {
548
549
}
550
551
@Override
552
public void onRemovedFromChannelNotification(String s) {
553
554
}
555
556
@Override
557
public void onNotificationSubscribed() {
558
559
}
560
561
@Override
562
public void onNotificationFailed(ErrorInfo errorInfo) {
563
564
}
565
}
566

Next, we need a default channel.


Join the General Channel

join-the-general-channel page anchor

This application will try to join a channel called "General Channel" when it starts. If the channel doesn't exist, it'll create one with that name. The scope of this example application will show you how to work only with public channels, but the Programmable Chat Client allows you to create private channels and handle invitations.

Notice we set a unique name for the general channel as we don't want to create a new general channel every time we start the application.

Join or Create a General Channel

join-or-create-a-general-channel page anchor

app/src/main/java/com/twilio/twiliochat/chat/channels/ChannelManager.java

1
package com.twilio.twiliochat.chat.channels;
2
3
import android.content.res.Resources;
4
import android.os.Handler;
5
import android.os.Looper;
6
import android.util.Log;
7
8
import com.twilio.chat.CallbackListener;
9
import com.twilio.chat.Channel;
10
import com.twilio.chat.Channel.ChannelType;
11
import com.twilio.chat.ChannelDescriptor;
12
import com.twilio.chat.Channels;
13
import com.twilio.chat.ChatClient;
14
import com.twilio.chat.ChatClientListener;
15
import com.twilio.chat.ErrorInfo;
16
import com.twilio.chat.Paginator;
17
import com.twilio.chat.StatusListener;
18
import com.twilio.chat.User;
19
import com.twilio.twiliochat.R;
20
import com.twilio.twiliochat.application.TwilioChatApplication;
21
import com.twilio.twiliochat.chat.ChatClientManager;
22
import com.twilio.twiliochat.chat.accesstoken.AccessTokenFetcher;
23
import com.twilio.twiliochat.chat.listeners.TaskCompletionListener;
24
25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.List;
28
29
public class ChannelManager implements ChatClientListener {
30
private static ChannelManager sharedManager = new ChannelManager();
31
public Channel generalChannel;
32
private ChatClientManager chatClientManager;
33
private ChannelExtractor channelExtractor;
34
private List<Channel> channels;
35
private Channels channelsObject;
36
private ChatClientListener listener;
37
private String defaultChannelName;
38
private String defaultChannelUniqueName;
39
private Handler handler;
40
private Boolean isRefreshingChannels = false;
41
42
private ChannelManager() {
43
this.chatClientManager = TwilioChatApplication.get().getChatClientManager();
44
this.channelExtractor = new ChannelExtractor();
45
this.listener = this;
46
defaultChannelName = getStringResource(R.string.default_channel_name);
47
defaultChannelUniqueName = getStringResource(R.string.default_channel_unique_name);
48
handler = setupListenerHandler();
49
}
50
51
public static ChannelManager getInstance() {
52
return sharedManager;
53
}
54
55
public List<Channel> getChannels() {
56
return channels;
57
}
58
59
public String getDefaultChannelName() {
60
return this.defaultChannelName;
61
}
62
63
public void leaveChannelWithHandler(Channel channel, StatusListener handler) {
64
channel.leave(handler);
65
}
66
67
public void deleteChannelWithHandler(Channel channel, StatusListener handler) {
68
channel.destroy(handler);
69
}
70
71
public void populateChannels(final LoadChannelListener listener) {
72
if (this.chatClientManager == null || this.isRefreshingChannels) {
73
return;
74
}
75
this.isRefreshingChannels = true;
76
77
handler.post(new Runnable() {
78
@Override
79
public void run() {
80
channelsObject = chatClientManager.getChatClient().getChannels();
81
82
channelsObject.getPublicChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
83
@Override
84
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
85
extractChannelsFromPaginatorAndPopulate(channelDescriptorPaginator, listener);
86
}
87
});
88
89
}
90
});
91
}
92
93
private void extractChannelsFromPaginatorAndPopulate(final Paginator<ChannelDescriptor> channelsPaginator,
94
final LoadChannelListener listener) {
95
channels = new ArrayList<>();
96
ChannelManager.this.channels.clear();
97
channelExtractor.extractAndSortFromChannelDescriptor(channelsPaginator,
98
new TaskCompletionListener<List<Channel>, String>() {
99
@Override
100
public void onSuccess(List<Channel> channels) {
101
ChannelManager.this.channels.addAll(channels);
102
Collections.sort(ChannelManager.this.channels, new CustomChannelComparator());
103
ChannelManager.this.isRefreshingChannels = false;
104
chatClientManager.addClientListener(ChannelManager.this);
105
listener.onChannelsFinishedLoading(ChannelManager.this.channels);
106
}
107
108
@Override
109
public void onError(String errorText) {
110
Log.e(TwilioChatApplication.TAG,"Error populating channels: " + errorText);
111
}
112
});
113
}
114
115
public void createChannelWithName(String name, final StatusListener handler) {
116
this.channelsObject
117
.channelBuilder()
118
.withFriendlyName(name)
119
.withType(ChannelType.PUBLIC)
120
.build(new CallbackListener<Channel>() {
121
@Override
122
public void onSuccess(final Channel newChannel) {
123
handler.onSuccess();
124
}
125
126
@Override
127
public void onError(ErrorInfo errorInfo) {
128
handler.onError(errorInfo);
129
}
130
});
131
}
132
133
public void joinOrCreateGeneralChannelWithCompletion(final StatusListener listener) {
134
channelsObject.getChannel(defaultChannelUniqueName, new CallbackListener<Channel>() {
135
@Override
136
public void onSuccess(Channel channel) {
137
ChannelManager.this.generalChannel = channel;
138
if (channel != null) {
139
joinGeneralChannelWithCompletion(listener);
140
} else {
141
createGeneralChannelWithCompletion(listener);
142
}
143
}
144
});
145
}
146
147
private void joinGeneralChannelWithCompletion(final StatusListener listener) {
148
if (generalChannel.getStatus() == Channel.ChannelStatus.JOINED) {
149
listener.onSuccess();
150
return;
151
}
152
this.generalChannel.join(new StatusListener() {
153
@Override
154
public void onSuccess() {
155
listener.onSuccess();
156
}
157
158
@Override
159
public void onError(ErrorInfo errorInfo) {
160
listener.onError(errorInfo);
161
}
162
});
163
}
164
165
private void createGeneralChannelWithCompletion(final StatusListener listener) {
166
this.channelsObject
167
.channelBuilder()
168
.withFriendlyName(defaultChannelName)
169
.withUniqueName(defaultChannelUniqueName)
170
.withType(ChannelType.PUBLIC)
171
.build(new CallbackListener<Channel>() {
172
@Override
173
public void onSuccess(final Channel channel) {
174
ChannelManager.this.generalChannel = channel;
175
ChannelManager.this.channels.add(channel);
176
joinGeneralChannelWithCompletion(listener);
177
}
178
179
@Override
180
public void onError(ErrorInfo errorInfo) {
181
listener.onError(errorInfo);
182
}
183
});
184
}
185
186
public void setChannelListener(ChatClientListener listener) {
187
this.listener = listener;
188
}
189
190
private String getStringResource(int id) {
191
Resources resources = TwilioChatApplication.get().getResources();
192
return resources.getString(id);
193
}
194
195
@Override
196
public void onChannelAdded(Channel channel) {
197
if (listener != null) {
198
listener.onChannelAdded(channel);
199
}
200
}
201
202
@Override
203
public void onChannelUpdated(Channel channel, Channel.UpdateReason updateReason) {
204
if (listener != null) {
205
listener.onChannelUpdated(channel, updateReason);
206
}
207
}
208
209
@Override
210
public void onChannelDeleted(Channel channel) {
211
if (listener != null) {
212
listener.onChannelDeleted(channel);
213
}
214
}
215
216
@Override
217
public void onChannelSynchronizationChange(Channel channel) {
218
if (listener != null) {
219
listener.onChannelSynchronizationChange(channel);
220
}
221
}
222
223
@Override
224
public void onError(ErrorInfo errorInfo) {
225
if (listener != null) {
226
listener.onError(errorInfo);
227
}
228
}
229
230
@Override
231
public void onClientSynchronization(ChatClient.SynchronizationStatus synchronizationStatus) {
232
233
}
234
235
@Override
236
public void onChannelJoined(Channel channel) {
237
238
}
239
240
@Override
241
public void onChannelInvited(Channel channel) {
242
243
}
244
245
@Override
246
public void onUserUpdated(User user, User.UpdateReason updateReason) {
247
if (listener != null) {
248
listener.onUserUpdated(user, updateReason);
249
}
250
}
251
252
@Override
253
public void onUserSubscribed(User user) {
254
255
}
256
257
@Override
258
public void onUserUnsubscribed(User user) {
259
260
}
261
262
@Override
263
public void onNewMessageNotification(String s, String s1, long l) {
264
265
}
266
267
@Override
268
public void onAddedToChannelNotification(String s) {
269
270
}
271
272
@Override
273
public void onInvitedToChannelNotification(String s) {
274
275
}
276
277
@Override
278
public void onRemovedFromChannelNotification(String s) {
279
280
}
281
282
@Override
283
public void onNotificationSubscribed() {
284
285
}
286
287
@Override
288
public void onNotificationFailed(ErrorInfo errorInfo) {
289
290
}
291
292
@Override
293
public void onConnectionStateChange(ChatClient.ConnectionState connectionState) {
294
295
}
296
297
@Override
298
public void onTokenExpired() {
299
refreshAccessToken();
300
}
301
302
@Override
303
public void onTokenAboutToExpire() {
304
refreshAccessToken();
305
}
306
307
private void refreshAccessToken() {
308
AccessTokenFetcher accessTokenFetcher = chatClientManager.getAccessTokenFetcher();
309
accessTokenFetcher.fetch(new TaskCompletionListener<String, String>() {
310
@Override
311
public void onSuccess(String token) {
312
ChannelManager.this.chatClientManager.getChatClient().updateToken(token, new StatusListener() {
313
@Override
314
public void onSuccess() {
315
Log.d(TwilioChatApplication.TAG, "Successfully updated access token.");
316
}
317
});
318
}
319
320
@Override
321
public void onError(String message) {
322
Log.e(TwilioChatApplication.TAG,"Error trying to fetch token: " + message);
323
}
324
});
325
}
326
327
private Handler setupListenerHandler() {
328
Looper looper;
329
Handler handler;
330
if ((looper = Looper.myLooper()) != null) {
331
handler = new Handler(looper);
332
} else if ((looper = Looper.getMainLooper()) != null) {
333
handler = new Handler(looper);
334
} else {
335
throw new IllegalArgumentException("Channel Listener must have a Looper.");
336
}
337
return handler;
338
}
339
}
340

Now let's listen for some channel events.


Listen to Channel Events

listen-to-channel-events page anchor

We set a channel's listener to MainChatFragment that implements ChannelListener, and here we implemented the following methods that listen to channel events:

  • onMessageAdded: When someone sends a message to the channel you are connected to.
  • onMemberAdded: When someone joins the channel.
  • onMemberDeleted: When someone leaves the channel.

As you may have noticed, each one of these methods includes useful objects as parameters. One example is the actual message that was added to the channel.

app/src/main/java/com/twilio/twiliochat/chat/MainChatFragment.java

1
package com.twilio.twiliochat.chat;
2
3
import android.app.Activity;
4
import android.content.Context;
5
import android.os.Bundle;
6
import androidx.fragment.app.Fragment;
7
import android.view.KeyEvent;
8
import android.view.LayoutInflater;
9
import android.view.View;
10
import android.view.ViewGroup;
11
import android.widget.Button;
12
import android.widget.EditText;
13
import android.widget.ListView;
14
import android.widget.TextView;
15
16
import com.twilio.chat.CallbackListener;
17
import com.twilio.chat.Channel;
18
import com.twilio.chat.ChannelListener;
19
import com.twilio.chat.ErrorInfo;
20
import com.twilio.chat.Member;
21
import com.twilio.chat.Message;
22
import com.twilio.chat.Messages;
23
import com.twilio.chat.StatusListener;
24
import com.twilio.twiliochat.R;
25
import com.twilio.twiliochat.chat.messages.JoinedStatusMessage;
26
import com.twilio.twiliochat.chat.messages.LeftStatusMessage;
27
import com.twilio.twiliochat.chat.messages.MessageAdapter;
28
import com.twilio.twiliochat.chat.messages.StatusMessage;
29
30
import java.util.List;
31
32
public class MainChatFragment extends Fragment implements ChannelListener {
33
Context context;
34
Activity mainActivity;
35
Button sendButton;
36
ListView messagesListView;
37
EditText messageTextEdit;
38
39
MessageAdapter messageAdapter;
40
Channel currentChannel;
41
Messages messagesObject;
42
43
public MainChatFragment() {
44
}
45
46
public static MainChatFragment newInstance() {
47
MainChatFragment fragment = new MainChatFragment();
48
return fragment;
49
}
50
51
@Override
52
public void onCreate(Bundle savedInstanceState) {
53
super.onCreate(savedInstanceState);
54
context = this.getActivity();
55
mainActivity = this.getActivity();
56
}
57
58
@Override
59
public View onCreateView(LayoutInflater inflater, ViewGroup container,
60
Bundle savedInstanceState) {
61
View view = inflater.inflate(R.layout.fragment_main_chat, container, false);
62
sendButton = (Button) view.findViewById(R.id.buttonSend);
63
messagesListView = (ListView) view.findViewById(R.id.listViewMessages);
64
messageTextEdit = (EditText) view.findViewById(R.id.editTextMessage);
65
66
messageAdapter = new MessageAdapter(mainActivity);
67
messagesListView.setAdapter(messageAdapter);
68
setUpListeners();
69
setMessageInputEnabled(false);
70
71
return view;
72
}
73
74
@Override
75
public void onAttach(Context context) {
76
super.onAttach(context);
77
}
78
79
@Override
80
public void onDetach() {
81
super.onDetach();
82
}
83
84
public Channel getCurrentChannel() {
85
return currentChannel;
86
}
87
88
public void setCurrentChannel(Channel currentChannel, final StatusListener handler) {
89
if (currentChannel == null) {
90
this.currentChannel = null;
91
return;
92
}
93
if (!currentChannel.equals(this.currentChannel)) {
94
setMessageInputEnabled(false);
95
this.currentChannel = currentChannel;
96
this.currentChannel.addListener(this);
97
if (this.currentChannel.getStatus() == Channel.ChannelStatus.JOINED) {
98
loadMessages(handler);
99
} else {
100
this.currentChannel.join(new StatusListener() {
101
@Override
102
public void onSuccess() {
103
loadMessages(handler);
104
}
105
106
@Override
107
public void onError(ErrorInfo errorInfo) {
108
}
109
});
110
}
111
}
112
}
113
114
private void loadMessages(final StatusListener handler) {
115
this.messagesObject = this.currentChannel.getMessages();
116
117
if (messagesObject != null) {
118
messagesObject.getLastMessages(100, new CallbackListener<List<Message>>() {
119
@Override
120
public void onSuccess(List<Message> messageList) {
121
messageAdapter.setMessages(messageList);
122
setMessageInputEnabled(true);
123
messageTextEdit.requestFocus();
124
handler.onSuccess();
125
}
126
});
127
}
128
}
129
130
private void setUpListeners() {
131
sendButton.setOnClickListener(new View.OnClickListener() {
132
@Override
133
public void onClick(View v) {
134
sendMessage();
135
}
136
});
137
messageTextEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
138
@Override
139
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
140
return false;
141
}
142
});
143
}
144
145
private void sendMessage() {
146
String messageText = getTextInput();
147
if (messageText.length() == 0) {
148
return;
149
}
150
Message.Options messageOptions = Message.options().withBody(messageText);
151
this.messagesObject.sendMessage(messageOptions, null);
152
clearTextInput();
153
}
154
155
private void setMessageInputEnabled(final boolean enabled) {
156
mainActivity.runOnUiThread(new Runnable() {
157
@Override
158
public void run() {
159
MainChatFragment.this.sendButton.setEnabled(enabled);
160
MainChatFragment.this.messageTextEdit.setEnabled(enabled);
161
}
162
});
163
}
164
165
private String getTextInput() {
166
return messageTextEdit.getText().toString();
167
}
168
169
private void clearTextInput() {
170
messageTextEdit.setText("");
171
}
172
173
@Override
174
public void onMessageAdded(Message message) {
175
messageAdapter.addMessage(message);
176
}
177
178
@Override
179
public void onMemberAdded(Member member) {
180
StatusMessage statusMessage = new JoinedStatusMessage(member.getIdentity());
181
this.messageAdapter.addStatusMessage(statusMessage);
182
}
183
184
@Override
185
public void onMemberDeleted(Member member) {
186
StatusMessage statusMessage = new LeftStatusMessage(member.getIdentity());
187
this.messageAdapter.addStatusMessage(statusMessage);
188
}
189
190
@Override
191
public void onMessageUpdated(Message message, Message.UpdateReason updateReason) {
192
}
193
194
@Override
195
public void onMessageDeleted(Message message) {
196
}
197
198
@Override
199
public void onMemberUpdated(Member member, Member.UpdateReason updateReason) {
200
}
201
202
@Override
203
public void onTypingStarted(Channel channel, Member member) {
204
}
205
206
@Override
207
public void onTypingEnded(Channel channel, Member member) {
208
}
209
210
@Override
211
public void onSynchronizationChanged(Channel channel) {
212
}
213
214
215
}
216

We've actually got a real chat app going here, but let's make it more interesting with multiple channels.


The application uses a Drawer Layout(link takes you to an external page) to show a list of the channels created for that Twilio account.

When you tap on the name of a channel, from the sidebar, that channel is set on the MainChatFragment. The setCurrentChannel method takes care of joining to the selected channel and loading the messages.

app/src/main/java/com/twilio/twiliochat/chat/MainChatFragment.java

1
package com.twilio.twiliochat.chat;
2
3
import android.app.Activity;
4
import android.content.Context;
5
import android.os.Bundle;
6
import androidx.fragment.app.Fragment;
7
import android.view.KeyEvent;
8
import android.view.LayoutInflater;
9
import android.view.View;
10
import android.view.ViewGroup;
11
import android.widget.Button;
12
import android.widget.EditText;
13
import android.widget.ListView;
14
import android.widget.TextView;
15
16
import com.twilio.chat.CallbackListener;
17
import com.twilio.chat.Channel;
18
import com.twilio.chat.ChannelListener;
19
import com.twilio.chat.ErrorInfo;
20
import com.twilio.chat.Member;
21
import com.twilio.chat.Message;
22
import com.twilio.chat.Messages;
23
import com.twilio.chat.StatusListener;
24
import com.twilio.twiliochat.R;
25
import com.twilio.twiliochat.chat.messages.JoinedStatusMessage;
26
import com.twilio.twiliochat.chat.messages.LeftStatusMessage;
27
import com.twilio.twiliochat.chat.messages.MessageAdapter;
28
import com.twilio.twiliochat.chat.messages.StatusMessage;
29
30
import java.util.List;
31
32
public class MainChatFragment extends Fragment implements ChannelListener {
33
Context context;
34
Activity mainActivity;
35
Button sendButton;
36
ListView messagesListView;
37
EditText messageTextEdit;
38
39
MessageAdapter messageAdapter;
40
Channel currentChannel;
41
Messages messagesObject;
42
43
public MainChatFragment() {
44
}
45
46
public static MainChatFragment newInstance() {
47
MainChatFragment fragment = new MainChatFragment();
48
return fragment;
49
}
50
51
@Override
52
public void onCreate(Bundle savedInstanceState) {
53
super.onCreate(savedInstanceState);
54
context = this.getActivity();
55
mainActivity = this.getActivity();
56
}
57
58
@Override
59
public View onCreateView(LayoutInflater inflater, ViewGroup container,
60
Bundle savedInstanceState) {
61
View view = inflater.inflate(R.layout.fragment_main_chat, container, false);
62
sendButton = (Button) view.findViewById(R.id.buttonSend);
63
messagesListView = (ListView) view.findViewById(R.id.listViewMessages);
64
messageTextEdit = (EditText) view.findViewById(R.id.editTextMessage);
65
66
messageAdapter = new MessageAdapter(mainActivity);
67
messagesListView.setAdapter(messageAdapter);
68
setUpListeners();
69
setMessageInputEnabled(false);
70
71
return view;
72
}
73
74
@Override
75
public void onAttach(Context context) {
76
super.onAttach(context);
77
}
78
79
@Override
80
public void onDetach() {
81
super.onDetach();
82
}
83
84
public Channel getCurrentChannel() {
85
return currentChannel;
86
}
87
88
public void setCurrentChannel(Channel currentChannel, final StatusListener handler) {
89
if (currentChannel == null) {
90
this.currentChannel = null;
91
return;
92
}
93
if (!currentChannel.equals(this.currentChannel)) {
94
setMessageInputEnabled(false);
95
this.currentChannel = currentChannel;
96
this.currentChannel.addListener(this);
97
if (this.currentChannel.getStatus() == Channel.ChannelStatus.JOINED) {
98
loadMessages(handler);
99
} else {
100
this.currentChannel.join(new StatusListener() {
101
@Override
102
public void onSuccess() {
103
loadMessages(handler);
104
}
105
106
@Override
107
public void onError(ErrorInfo errorInfo) {
108
}
109
});
110
}
111
}
112
}
113
114
private void loadMessages(final StatusListener handler) {
115
this.messagesObject = this.currentChannel.getMessages();
116
117
if (messagesObject != null) {
118
messagesObject.getLastMessages(100, new CallbackListener<List<Message>>() {
119
@Override
120
public void onSuccess(List<Message> messageList) {
121
messageAdapter.setMessages(messageList);
122
setMessageInputEnabled(true);
123
messageTextEdit.requestFocus();
124
handler.onSuccess();
125
}
126
});
127
}
128
}
129
130
private void setUpListeners() {
131
sendButton.setOnClickListener(new View.OnClickListener() {
132
@Override
133
public void onClick(View v) {
134
sendMessage();
135
}
136
});
137
messageTextEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
138
@Override
139
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
140
return false;
141
}
142
});
143
}
144
145
private void sendMessage() {
146
String messageText = getTextInput();
147
if (messageText.length() == 0) {
148
return;
149
}
150
Message.Options messageOptions = Message.options().withBody(messageText);
151
this.messagesObject.sendMessage(messageOptions, null);
152
clearTextInput();
153
}
154
155
private void setMessageInputEnabled(final boolean enabled) {
156
mainActivity.runOnUiThread(new Runnable() {
157
@Override
158
public void run() {
159
MainChatFragment.this.sendButton.setEnabled(enabled);
160
MainChatFragment.this.messageTextEdit.setEnabled(enabled);
161
}
162
});
163
}
164
165
private String getTextInput() {
166
return messageTextEdit.getText().toString();
167
}
168
169
private void clearTextInput() {
170
messageTextEdit.setText("");
171
}
172
173
@Override
174
public void onMessageAdded(Message message) {
175
messageAdapter.addMessage(message);
176
}
177
178
@Override
179
public void onMemberAdded(Member member) {
180
StatusMessage statusMessage = new JoinedStatusMessage(member.getIdentity());
181
this.messageAdapter.addStatusMessage(statusMessage);
182
}
183
184
@Override
185
public void onMemberDeleted(Member member) {
186
StatusMessage statusMessage = new LeftStatusMessage(member.getIdentity());
187
this.messageAdapter.addStatusMessage(statusMessage);
188
}
189
190
@Override
191
public void onMessageUpdated(Message message, Message.UpdateReason updateReason) {
192
}
193
194
@Override
195
public void onMessageDeleted(Message message) {
196
}
197
198
@Override
199
public void onMemberUpdated(Member member, Member.UpdateReason updateReason) {
200
}
201
202
@Override
203
public void onTypingStarted(Channel channel, Member member) {
204
}
205
206
@Override
207
public void onTypingEnded(Channel channel, Member member) {
208
}
209
210
@Override
211
public void onSynchronizationChanged(Channel channel) {
212
}
213
214
215
}
216

If we can join other channels, we'll need some way for a super user to create new channels (and delete old ones).


We use an input dialog so the user can type the name of the new channel. The only restriction here is that the user can't create a channel called "General Channel". Other than that, creating a channel involves using the channelBuilder as shown and providing at the very least a channel type.

You can provide additional parameters to the builder as we did with a general channel to set a unique name. There's a list of methods you can use in the client library API docs(link takes you to an external page).

app/src/main/java/com/twilio/twiliochat/chat/channels/ChannelManager.java

1
package com.twilio.twiliochat.chat.channels;
2
3
import android.content.res.Resources;
4
import android.os.Handler;
5
import android.os.Looper;
6
import android.util.Log;
7
8
import com.twilio.chat.CallbackListener;
9
import com.twilio.chat.Channel;
10
import com.twilio.chat.Channel.ChannelType;
11
import com.twilio.chat.ChannelDescriptor;
12
import com.twilio.chat.Channels;
13
import com.twilio.chat.ChatClient;
14
import com.twilio.chat.ChatClientListener;
15
import com.twilio.chat.ErrorInfo;
16
import com.twilio.chat.Paginator;
17
import com.twilio.chat.StatusListener;
18
import com.twilio.chat.User;
19
import com.twilio.twiliochat.R;
20
import com.twilio.twiliochat.application.TwilioChatApplication;
21
import com.twilio.twiliochat.chat.ChatClientManager;
22
import com.twilio.twiliochat.chat.accesstoken.AccessTokenFetcher;
23
import com.twilio.twiliochat.chat.listeners.TaskCompletionListener;
24
25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.List;
28
29
public class ChannelManager implements ChatClientListener {
30
private static ChannelManager sharedManager = new ChannelManager();
31
public Channel generalChannel;
32
private ChatClientManager chatClientManager;
33
private ChannelExtractor channelExtractor;
34
private List<Channel> channels;
35
private Channels channelsObject;
36
private ChatClientListener listener;
37
private String defaultChannelName;
38
private String defaultChannelUniqueName;
39
private Handler handler;
40
private Boolean isRefreshingChannels = false;
41
42
private ChannelManager() {
43
this.chatClientManager = TwilioChatApplication.get().getChatClientManager();
44
this.channelExtractor = new ChannelExtractor();
45
this.listener = this;
46
defaultChannelName = getStringResource(R.string.default_channel_name);
47
defaultChannelUniqueName = getStringResource(R.string.default_channel_unique_name);
48
handler = setupListenerHandler();
49
}
50
51
public static ChannelManager getInstance() {
52
return sharedManager;
53
}
54
55
public List<Channel> getChannels() {
56
return channels;
57
}
58
59
public String getDefaultChannelName() {
60
return this.defaultChannelName;
61
}
62
63
public void leaveChannelWithHandler(Channel channel, StatusListener handler) {
64
channel.leave(handler);
65
}
66
67
public void deleteChannelWithHandler(Channel channel, StatusListener handler) {
68
channel.destroy(handler);
69
}
70
71
public void populateChannels(final LoadChannelListener listener) {
72
if (this.chatClientManager == null || this.isRefreshingChannels) {
73
return;
74
}
75
this.isRefreshingChannels = true;
76
77
handler.post(new Runnable() {
78
@Override
79
public void run() {
80
channelsObject = chatClientManager.getChatClient().getChannels();
81
82
channelsObject.getPublicChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
83
@Override
84
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
85
extractChannelsFromPaginatorAndPopulate(channelDescriptorPaginator, listener);
86
}
87
});
88
89
}
90
});
91
}
92
93
private void extractChannelsFromPaginatorAndPopulate(final Paginator<ChannelDescriptor> channelsPaginator,
94
final LoadChannelListener listener) {
95
channels = new ArrayList<>();
96
ChannelManager.this.channels.clear();
97
channelExtractor.extractAndSortFromChannelDescriptor(channelsPaginator,
98
new TaskCompletionListener<List<Channel>, String>() {
99
@Override
100
public void onSuccess(List<Channel> channels) {
101
ChannelManager.this.channels.addAll(channels);
102
Collections.sort(ChannelManager.this.channels, new CustomChannelComparator());
103
ChannelManager.this.isRefreshingChannels = false;
104
chatClientManager.addClientListener(ChannelManager.this);
105
listener.onChannelsFinishedLoading(ChannelManager.this.channels);
106
}
107
108
@Override
109
public void onError(String errorText) {
110
Log.e(TwilioChatApplication.TAG,"Error populating channels: " + errorText);
111
}
112
});
113
}
114
115
public void createChannelWithName(String name, final StatusListener handler) {
116
this.channelsObject
117
.channelBuilder()
118
.withFriendlyName(name)
119
.withType(ChannelType.PUBLIC)
120
.build(new CallbackListener<Channel>() {
121
@Override
122
public void onSuccess(final Channel newChannel) {
123
handler.onSuccess();
124
}
125
126
@Override
127
public void onError(ErrorInfo errorInfo) {
128
handler.onError(errorInfo);
129
}
130
});
131
}
132
133
public void joinOrCreateGeneralChannelWithCompletion(final StatusListener listener) {
134
channelsObject.getChannel(defaultChannelUniqueName, new CallbackListener<Channel>() {
135
@Override
136
public void onSuccess(Channel channel) {
137
ChannelManager.this.generalChannel = channel;
138
if (channel != null) {
139
joinGeneralChannelWithCompletion(listener);
140
} else {
141
createGeneralChannelWithCompletion(listener);
142
}
143
}
144
});
145
}
146
147
private void joinGeneralChannelWithCompletion(final StatusListener listener) {
148
if (generalChannel.getStatus() == Channel.ChannelStatus.JOINED) {
149
listener.onSuccess();
150
return;
151
}
152
this.generalChannel.join(new StatusListener() {
153
@Override
154
public void onSuccess() {
155
listener.onSuccess();
156
}
157
158
@Override
159
public void onError(ErrorInfo errorInfo) {
160
listener.onError(errorInfo);
161
}
162
});
163
}
164
165
private void createGeneralChannelWithCompletion(final StatusListener listener) {
166
this.channelsObject
167
.channelBuilder()
168
.withFriendlyName(defaultChannelName)
169
.withUniqueName(defaultChannelUniqueName)
170
.withType(ChannelType.PUBLIC)
171
.build(new CallbackListener<Channel>() {
172
@Override
173
public void onSuccess(final Channel channel) {
174
ChannelManager.this.generalChannel = channel;
175
ChannelManager.this.channels.add(channel);
176
joinGeneralChannelWithCompletion(listener);
177
}