package eu.hgross.blaubot.example.chat.views; import android.app.AlertDialog; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.text.Editable; import android.text.TextWatcher; import android.util.AttributeSet; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ScrollView; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import eu.hgross.blaubot.core.Blaubot; import eu.hgross.blaubot.core.IBlaubotDevice; import eu.hgross.blaubot.core.ILifecycleListener; import eu.hgross.blaubot.core.statemachine.IBlaubotConnectionStateMachineListener; import eu.hgross.blaubot.core.statemachine.states.FreeState; import eu.hgross.blaubot.core.statemachine.states.IBlaubotState; import eu.hgross.blaubot.example.chat.messages.ChatUser; import eu.hgross.blaubot.example.chat.R; import eu.hgross.blaubot.example.chat.messages.ChatMessage; import eu.hgross.blaubot.example.chat.messages.HelloMessage; import eu.hgross.blaubot.example.chat.messages.NameChangeMessage; import eu.hgross.blaubot.messaging.BlaubotMessage; import eu.hgross.blaubot.messaging.IBlaubotChannel; import eu.hgross.blaubot.messaging.IBlaubotMessageListener; /** * A Chat view displaying ChatMessages send through a channel. * Utilizes channels for different purposes like * <ul> * <li>HelloMessages, that are send when a device enters a ChatRoom</li> * <li>ChatMessages, that are the actual chat messages</li> * <li>NameChangeMessages, which announce a nickname change</li> * </ul> * * Additionally listens to Blaubot's lifecycle to get to know when devices are * not part of the blaubot network anymore. */ public class ChatRoomView extends ScrollView implements ILifecycleListener, IBlaubotConnectionStateMachineListener { /** * Max number of chat messages to be displayed. Older ones will be discarded. */ private static final int DEFAULT_MAX_MESSAGES = 150; private IBlaubotChannel mMessageChannel; private IBlaubotChannel mNameChangeChannel; private IBlaubotChannel mHelloMessageChannel; private int maxMessages = DEFAULT_MAX_MESSAGES; private Handler mUiHandler; private List<View> mMessageViews; /** * Maps unique device ids to nicknames. */ private ConcurrentHashMap<String, String> mDeviceToNicknameMapping; private Blaubot mBlaubot; private ChatUser mChatUser; private LinearLayout mMainView; public ChatRoomView(Context context, AttributeSet attributeSet) { super(context, attributeSet); initView(context, attributeSet); } private void initView(Context context, AttributeSet attributeSet) { mMessageViews = new LinkedList<>(); mUiHandler = new Handler(Looper.getMainLooper()); mDeviceToNicknameMapping = new ConcurrentHashMap<>(); // setOrientation(VERTICAL); mMainView = new LinearLayout(context, attributeSet); mMainView.setOrientation(LinearLayout.VERTICAL); addView(mMainView); } /** * Adds views to the main panel and maintains the max number of views defined * * @param v the view to add */ private void addViewToChatRoom(View v) { mMessageViews.add(v); mMainView.addView(v); if (mMessageViews.size() > maxMessages) { // remove head View removedView = mMessageViews.remove(0); mMainView.removeView(removedView); } mUiHandler.post(new Runnable() { @Override public void run() { // scroll down fullScroll(View.FOCUS_DOWN); } }); } /** * Updates our udid to nickname mapping * * @param deviceUuid the udid * @param nickName the nickname */ private void maintainUniqueDeviceIdToNicknameMapping(String deviceUuid, String nickName) { mDeviceToNicknameMapping.put(deviceUuid, nickName); } /** * Handles incoming chat messages */ private IBlaubotMessageListener mMessageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { final ChatMessage chatMessage = ChatMessage.fromBytes(blaubotMessage.getPayload()); mUiHandler.post(new Runnable() { @Override public void run() { ChatMessageView chatMessageView = new ChatMessageView(getContext(), chatMessage); addViewToChatRoom(chatMessageView); ChatUser originator = chatMessage.getOriginator(); maintainUniqueDeviceIdToNicknameMapping(originator.getDeviceUuid(), originator.getUserName()); } }); } }; /** * Handles incoming NameChangeMessages */ private IBlaubotMessageListener mNameChangeMessageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { final NameChangeMessage nameChangeMessage = NameChangeMessage.fromBytes(blaubotMessage.getPayload()); mUiHandler.post(new Runnable() { @Override public void run() { NameChangeMessageView view = new NameChangeMessageView(getContext(), nameChangeMessage); addViewToChatRoom(view); maintainUniqueDeviceIdToNicknameMapping(nameChangeMessage.getDeviceUuid(), nameChangeMessage.getNewName()); } }); } }; /** * Handles incoming hello messages */ private IBlaubotMessageListener mHelloMessageMessageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { final HelloMessage helloMessage = HelloMessage.fromBytes(blaubotMessage.getPayload()); mUiHandler.post(new Runnable() { @Override public void run() { HelloMessageView helloMessageView = new HelloMessageView(getContext(), helloMessage); addViewToChatRoom(helloMessageView); maintainUniqueDeviceIdToNicknameMapping(helloMessage.getOriginator().getDeviceUuid(), helloMessage.getOriginator().getUserName()); } }); } }; /** * Registers the channel on which the messages are received * * @param blaubotChannel the channel to register to */ private void registerMessageChannel(IBlaubotChannel blaubotChannel) { mMessageChannel = blaubotChannel; mMessageChannel.subscribe(mMessageListener); } /** * Registers the channel on which name changes are received * * @param blaubotChannel the channel to register to */ private void registerNameChangeChannel(IBlaubotChannel blaubotChannel) { mNameChangeChannel = blaubotChannel; mNameChangeChannel.subscribe(mNameChangeMessageListener); } /** * Registers the channel on which new users say hello on entry * * @param blaubotChannel the channel to register to */ private void registerHelloMessageChannel(IBlaubotChannel blaubotChannel) { mHelloMessageChannel = blaubotChannel; mHelloMessageChannel.subscribe(mHelloMessageMessageListener); } /** * Unregisters from all channels, if any channel is regsitered. */ private void unregisterChannels() { if (mMessageChannel != null) { mMessageChannel.removeMessageListener(mMessageListener); } if (mNameChangeChannel != null) { mNameChangeChannel.removeMessageListener(mNameChangeMessageListener); } if (mHelloMessageChannel != null) { mHelloMessageChannel.removeMessageListener(mHelloMessageMessageListener); } } /** * Registers this instance with blaubot * * @param blaubot the blaubot instance * @param messageChannelId the channel id to use for sending and receiving chat messages * @param helloChannelId the channel id to use for sending and receiving hello announcements * @param nameChangeChannelId the channel id to use for sending and receiving name changes */ public void registerBlaubot(Blaubot blaubot, short messageChannelId, short helloChannelId, short nameChangeChannelId) { if (mBlaubot != null) { unregisterBlaubot(); } mBlaubot = blaubot; mBlaubot.addLifecycleListener(this); mBlaubot.getConnectionStateMachine().addConnectionStateMachineListener(this); mChatUser = new ChatUser(); mChatUser.setDeviceUuid(mBlaubot.getOwnDevice().getUniqueDeviceID()); mChatUser.setUserName(android.os.Build.MODEL + ""); // Create a channel for each of our messages mMessageChannel = mBlaubot.createChannel(messageChannelId); mNameChangeChannel = mBlaubot.createChannel(nameChangeChannelId); mHelloMessageChannel = mBlaubot.createChannel(helloChannelId); // register them with the chatroom registerMessageChannel(mMessageChannel); registerNameChangeChannel(mNameChangeChannel); registerHelloMessageChannel(mHelloMessageChannel); } /** * Unregister this instance from blaubot */ public void unregisterBlaubot() { unregisterChannels(); if (mBlaubot != null) { mBlaubot.removeLifecycleListener(this); mBlaubot.getConnectionStateMachine().removeConnectionStateMachineListener(this); mBlaubot = null; } } @Override public void onConnected() { mUiHandler.post(new Runnable() { @Override public void run() { // add left message String message = getResources().getString(R.string.connected_to_chatroom); View view = Util.createGenericChatMessageView(getContext(), System.currentTimeMillis(), message); addViewToChatRoom(view); } }); sendHello(); } private void sendHello() { // say hello HelloMessage helloMessage = new HelloMessage(); helloMessage.setOriginator(mChatUser); mHelloMessageChannel.publish(helloMessage.toBytes()); } @Override public void onDisconnected() { mUiHandler.post(new Runnable() { @Override public void run() { // add left message String message = getResources().getString(R.string.disconnected_from_chatroom); View view = Util.createGenericChatMessageView(getContext(), System.currentTimeMillis(), message); addViewToChatRoom(view); } }); } @Override public void onDeviceJoined(IBlaubotDevice blaubotDevice) { // handled by hello messages } @Override public void onDeviceLeft(IBlaubotDevice blaubotDevice) { final String nickname = resolveNickname(blaubotDevice.getUniqueDeviceID()); mUiHandler.post(new Runnable() { @Override public void run() { // add left message String message = nickname + " " + getResources().getString(R.string.left_chatroom_message); View view = Util.createGenericChatMessageView(getContext(), System.currentTimeMillis(), message); addViewToChatRoom(view); } }); } @Override public void onPrinceDeviceChanged(IBlaubotDevice oldPrince, IBlaubotDevice newPrince) { } @Override public void onKingDeviceChanged(IBlaubotDevice oldKing, IBlaubotDevice newKing) { if (oldKing == null) { // if we were not part of a network before, we did not merge but connected initially return; } mUiHandler.post(new Runnable() { @Override public void run() { // add left message String message = getResources().getString(R.string.merged_with_nearby_chatroom); View view = Util.createGenericChatMessageView(getContext(), System.currentTimeMillis(), message); addViewToChatRoom(view); } }); sendHello(); } @Override public void onStateChanged(IBlaubotState oldState, IBlaubotState newState) { if (newState instanceof FreeState) { mUiHandler.post(new Runnable() { @Override public void run() { // add left message String message = getResources().getString(R.string.searching_chatroom); View view = Util.createGenericChatMessageView(getContext(), System.currentTimeMillis(), message); addViewToChatRoom(view); } }); mMessageChannel.clearMessageQueue(); mHelloMessageChannel.clearMessageQueue(); mNameChangeChannel.clearMessageQueue(); } } @Override public void onStateMachineStopped() { mUiHandler.post(new Runnable() { @Override public void run() { // add left message String message = getResources().getString(R.string.stopped); View view = Util.createGenericChatMessageView(getContext(), System.currentTimeMillis(), message); addViewToChatRoom(view); } }); } @Override public void onStateMachineStarted() { } /** * Tries to resolve the nickname for a unique device id * * @param uniqueDeviceId the unique device id * @return the nickanme or the unique device id */ private String resolveNickname(String uniqueDeviceId) { // try to retrieve nickname String nickname = mDeviceToNicknameMapping.get(uniqueDeviceId); final String fnickname = nickname != null && !nickname.isEmpty() ? nickname : uniqueDeviceId; return fnickname; } /** * Creates and shows the dialog to change the nickname. */ public void showChangeNicknameDialog() { View editView = inflate(getContext(), R.layout.change_username_editview, null); AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle(R.string.change_nickname_dialog_title).setView(editView); final AlertDialog dialog = builder.create(); final Button doChangeButton = (Button) editView.findViewById(R.id.doChangeButton); final Button cancelButton = (Button) editView.findViewById(R.id.doCancelButton); final EditText nickNameEditText = (EditText) editView.findViewById(R.id.username); nickNameEditText.setText(mChatUser.getUserName()); nickNameEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { boolean valid = validateUsername(s.toString()); if (!valid) { nickNameEditText.setError(getResources().getString(R.string.invalid_username)); } else { nickNameEditText.setError(null); } } @Override public void afterTextChanged(Editable s) { } }); doChangeButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String newUsername = nickNameEditText.getText().toString(); // validate username if (!validateUsername(newUsername)) { return; } // preserver old name String oldName = mChatUser.getUserName(); // set username mChatUser.setUserName(newUsername); // inform others NameChangeMessage nameChangeMessage = new NameChangeMessage(); nameChangeMessage.setDeviceUuid(mBlaubot.getOwnDevice().getUniqueDeviceID()); nameChangeMessage.setPreviousName(oldName); nameChangeMessage.setNewName(newUsername); mNameChangeChannel.publish(nameChangeMessage.toBytes()); // close dialog dialog.dismiss(); } }); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dialog.dismiss(); } }); dialog.show(); } /** * Validates a username to be non empty * * @param username the username to check * @return true, if valid username */ private boolean validateUsername(String username) { return username != null && !username.isEmpty(); } /** * Sends a chat message * * @param text the text * @return true, iff message was queued to send */ public boolean sendChatMessage(String text) { ChatMessage message = new ChatMessage(); message.setOriginator(mChatUser); message.setMessage(text); boolean queued = mMessageChannel.publish(message.toBytes()); return queued; } }