package com.buddy.sample.buddychat;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import com.buddy.sdk.Buddy;
import com.buddy.sdk.BuddyCallback;
import com.buddy.sdk.BuddyResult;
import com.buddy.sdk.DateRange;
import com.buddy.sdk.models.Message;
import com.buddy.sdk.models.PagedResult;
import com.buddy.sdk.models.User;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
// This is the main activity that does the chatting
//
// This activity listens for notifications from the device that we are chatting with
// and updates it's state accordingly. This uses the Buddy push notification APIs.
//
// The notifications tell this chat app what the state is and what to do next so that
// the client doesn't have to poll for changes.
//
//
public class Chat extends AppCompatActivity {
public static final String TAG = "Chat";
private enum STATE {
DISCONNECTED,
WAITING,
CHATTING
}
private STATE state = STATE.DISCONNECTED;
// tell the other client to check for new messages.
private static final String CHECK_MESSAGES = "check_messsages";
// tell the other client that we are waiting to chat with them
private static final String CHAT_WAITING = "chat_waiting";
// tell the other client that we are happily connected
private static final String CHAT_CONNECTED = "chat_connected";
// tell the other client that we are leaving the session;
private static final String CHAT_END = "chat_end";
// information about the user we are chatting with
String userId;
String userName;
String myUserId;
String myUserName;
// the thread id of this chat, which is a combo of
// the logged in user's ID and the remote chat user's ID
String threadId;
List<String> toList = new ArrayList<String>();
Date lastPing;
Timer timer;
ListView _messages;
MessagesSimpleAdapter _adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat);
lastPing = new Date();
BuddyChatApplication.instance.getCurrentUser(false, new GetCurrentUserCallback() {
@Override
public void complete(User user) {
if (user != null) {
myUserId = user.id;
myUserName = user.userName;
Intent i = getIntent();
userId = i.getStringExtra("userId");
userName = i.getStringExtra("userName");
if (userId == null) {
// something bad happened
Log.e(TAG, "No current user: " + i.getExtras().toString());
finish();
return;
}
toList.add(userId);
if (userName == null) {
// we didn't get a user name (usually due to being launched by a notification)
Buddy.get("/users/" + userId, null, new BuddyCallback<User>(User.class) {
@Override
public void completed(BuddyResult<User> result) {
if (result.getIsSuccess()) {
userName = result.getResult().userName;
}
initialize();
}
});
} else {
initialize();
}
}
}
});
}
private void initialize() {
// create a thread ID that is unique between
// each chat party, that is also unique.
// alphabetizing and concat'ing is an easy solution:
List<String> ids = new ArrayList<String>();
ids.add(BuddyChatApplication.instance.currentUser.id);
ids.add(userId);
Collections.sort(ids);
threadId = String.format("%s-%s", ids.get(0), ids.get(1));
// set up our UI
Button btnSend = (Button) findViewById(R.id.btnSend);
final EditText textMsg = (EditText) findViewById(R.id.textMsg);
setState(STATE.WAITING);
// handle click of the send button
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// pull together the params for the message
final Map<String, Object> params = new HashMap<String, Object>();
params.put("thread", threadId);
params.put("addressees", toList);
params.put("subject", "chat");
params.put("body", textMsg.getText().toString());
if (state != STATE.CHATTING) {
setState(STATE.WAITING);
}
Buddy.post("/messages", params, new BuddyCallback<Message>(Message.class) {
@Override
public void completed(BuddyResult<Message> result) {
// if the send is successful
if (result.getIsSuccess()) {
// send a push notifying that we've sent a new message
// which means the other client doesn't need to poll.
//
// reset our waiting counter.
lastPing = new Date();
// send the notification so the client knows to check for messages
//
sendPushNotification(CHECK_MESSAGES, "Buddy Chat", String.format("%s: %s", myUserName, textMsg.getText().toString()));
textMsg.setText("");
// refresh the messages to include the one we just sent
loadMessages();
} else {
// TODO: properly handle a send failure...hey, it's just a sample.
Log.e(TAG, "Error sending message: " + result.getError());
}
}
});
}
});
// set up the messages list
_messages = (ListView) findViewById(R.id.lvMessages);
_adapter = new MessagesSimpleAdapter(getBaseContext());
_messages.setAdapter(_adapter);
// tell the other client we are waiting on them.
sendPushNotification(CHAT_WAITING, null, null);
loadMessages();
}
Date lastMessageDate;
boolean loadingMessages;
// refresh the list of messages
//
private void loadMessages() {
if (loadingMessages) {
return;
}
Map<String, Object> params = new HashMap<String, Object>();
// specify the thread we are on
params.put("thread", threadId);
// we pull the chats newest first so old ones just page off the top
params.put("sortOrder", "-created");
// only get the 20 most recent messages
params.put("pagingToken", "20;0");
if (lastMessageDate != null) {
DateRange dr = new DateRange(new Date(lastMessageDate.getTime() + 1000), new Date(new Date().getTime() + 60000));
params.put("created", dr);
}
loadingMessages = true;
Buddy.get("/messages", params, new BuddyCallback<PagedResult>(PagedResult.class) {
@Override
public void completed(BuddyResult<PagedResult> result) {
loadingMessages = false;
if (result.getIsSuccess()) {
List<Message> msgList = result.getResult().convertPageResults(Message.class);
// reverse the list so it's oldest first
Collections.reverse(msgList);
// get the date of the last message we retrieved,
// and use this as the starting point for the next query so we don't always
// ask for the whole batch.
if (msgList.size() > 0) {
lastMessageDate = msgList.get(msgList.size() - 1).created;
}
// look for the newest received message, and use that as the last ping.
//
for (Message m : msgList) {
if (m.type == Message.MessageType.Received) {
if (lastPing == null || m.created.getTime() > lastPing.getTime()) {
lastPing = m.created;
}
break;
}
}
_adapter.appendItemList(msgList);
_adapter.notifyDataSetChanged();
scrollMyListViewToBottom();
}
}
});
}
// sets the current chat state
private void setState(STATE newState) {
if (this.state == newState) return;
String strState = null;
switch (newState) {
case CHATTING:
strState = "Connected";
break;
case WAITING:
strState = "Waiting";
break;
case DISCONNECTED:
strState = "Disconnected";
break;
}
TextView lblMessage = (TextView) findViewById(R.id.lblMsg);
lblMessage.setText(String.format("Chatting with %s (%s)", userName, strState));
this.state = newState;
}
// helper for sending notifications to the other client
private void sendPushNotification(String type, String title, String message) {
String payload = String.format("%s\t%s", type, myUserId);
Buddy.sendPushNotification(toList, title, message, payload);
}
public static final int PART_MESSAGE = 0;
public static final int PART_USERID = 1;
public static String[] crackPayload(String payload) {
String[] parts = payload.split("\t");
return parts;
}
// Receiver for messages, push notification values from the other client
// end up in here
private BroadcastReceiver onEvent = new BroadcastReceiver() {
public void onReceive(Context ctx, Intent i) {
// make sure we have a payload and a user id
String payload = i.getStringExtra("payload");
if (payload == null) {
return;
}
String[] parts = crackPayload(payload);
String msg = parts[PART_MESSAGE];
String id = parts[PART_USERID];
// this isn't meant for this chat, ignore.
if (!userId.equals(id)) {
return;
}
// set the time that we had last comms from the other client
lastPing = new Date();
// walk through the message types
if (CHECK_MESSAGES.equals(msg)) {
setState(STATE.CHATTING);
loadMessages();
} else if (CHAT_WAITING.equals(msg)) {
// the client is waiting for us, so we know
// they are connected. Send a push response.
setState(STATE.CHATTING);
sendPushNotification(CHAT_CONNECTED, null, null);
} else if (CHAT_CONNECTED.equals(msg)) {
// client has told us they are connected,
// transition to chatting
setState(STATE.CHATTING);
} else if (CHAT_END.equals(msg)) {
setState(STATE.DISCONNECTED);
}
}
};
@Override
public void onResume() {
super.onResume();
BuddyChatApplication.activeChat = this;
IntentFilter f = new IntentFilter(GcmListenerService.ACTION_MESSAGE_RECEIVED);
LocalBroadcastManager.getInstance(this)
.registerReceiver(onEvent, f);
// send a message letting the other client know that we are waiting
sendPushNotification(CHAT_WAITING, null, null);
// finally, set up a timer to check status every 15 seconds.
timer = new Timer();
ChatUpdateTask task = new ChatUpdateTask(this);
// this is sort of for emulator only - push should always
// be supported otherwise, but in this case, check every 15 seconds
timer.schedule(task, 100, 15000);
}
@Override
public void onPause() {
// on pause we unhook our receiver and let the other client know we're not
// chatting anymore.
LocalBroadcastManager.getInstance(this)
.unregisterReceiver(onEvent);
sendPushNotification(CHAT_END, null, null);
BuddyChatApplication.activeChat = null;
timer.cancel();
timer = null;
super.onPause();
}
// for updating on a timer, we user this task to poke at load messages
// on a schedule
class ChatUpdateTask extends TimerTask {
Chat parent;
public ChatUpdateTask(Chat parent) {
this.parent = parent;
}
public void run() {
parent.runOnUiThread(new Runnable() {
public void run() {
if (BuddyChatApplication.activeChat != ChatUpdateTask.this.parent) {
return;
}
// tell the other client we are still here.
long lastPingDelta = new Date().getTime() - lastPing.getTime();
if (lastPingDelta > 120000) {
// after two minutes, consider ourselves disconnected.
sendPushNotification(CHAT_WAITING, null, null);
setState(STATE.DISCONNECTED);
} else if (lastPingDelta > 60000) {
setState(STATE.WAITING);
sendPushNotification(CHAT_WAITING, null, null);
}
}
});
}
}
private void scrollMyListViewToBottom() {
// when messages come in,
// scroll to bottom to show new ones
_messages.post(new Runnable() {
@Override
public void run() {
// Select the last row so it will scroll into view...
_messages.setSelection(_messages.getCount() - 1);
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.chat, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public android.support.v4.app.FragmentManager getSupportFragmentManager() {
return null;
}
class MessagesSimpleAdapter extends SimpleAdapter<Message> {
public MessagesSimpleAdapter(Context c) {
super(null, c);
}
protected <T> void populateView(View v, T u) {
Message m = (Message) u;
TextView text2 = (TextView) v.findViewById(android.R.id.text1);
TextView text1 = (TextView) v.findViewById(android.R.id.text2);
if (m.type == Message.MessageType.Received) {
text1.setText(String.format("%s (%s)", userName, android.text.format.DateFormat.format("MM-dd hh:mm", m.created)));
} else {
text1.setText(String.format("You (%s)", android.text.format.DateFormat.format("MM-dd hh:mm", m.created)));
}
text2.setText(m.body);
}
}
}