/*************************************************************************** * Copyright 2006-2016 by Christian Ihle * * contact@kouchat.net * * * * This file is part of KouChat. * * * * KouChat is free software; you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation, either version 3 of * * the License, or (at your option) any later version. * * * * KouChat is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with KouChat. * * If not, see <http://www.gnu.org/licenses/>. * ***************************************************************************/ package net.usikkert.kouchat.android.chatwindow; import java.io.File; import java.util.concurrent.ExecutionException; import net.usikkert.kouchat.Constants; import net.usikkert.kouchat.android.R; import net.usikkert.kouchat.android.component.Command; import net.usikkert.kouchat.android.component.CommandWithToastOnExceptionAsyncTask; import net.usikkert.kouchat.android.controller.MainChatController; import net.usikkert.kouchat.android.filetransfer.AndroidFileTransferListener; import net.usikkert.kouchat.android.filetransfer.AndroidFileUtils; import net.usikkert.kouchat.android.notification.NotificationService; import net.usikkert.kouchat.android.settings.AndroidSettings; import net.usikkert.kouchat.android.settings.AndroidSettingsSaver; import net.usikkert.kouchat.event.NetworkConnectionListener; import net.usikkert.kouchat.message.CoreMessages; import net.usikkert.kouchat.misc.ChatLogger; import net.usikkert.kouchat.misc.CommandException; import net.usikkert.kouchat.misc.CommandParser; import net.usikkert.kouchat.misc.Controller; import net.usikkert.kouchat.misc.ErrorHandler; import net.usikkert.kouchat.misc.MessageController; import net.usikkert.kouchat.misc.Topic; import net.usikkert.kouchat.misc.User; import net.usikkert.kouchat.misc.UserList; import net.usikkert.kouchat.net.FileReceiver; import net.usikkert.kouchat.net.FileSender; import net.usikkert.kouchat.net.FileToSend; import net.usikkert.kouchat.net.FileTransfer; import net.usikkert.kouchat.net.TransferList; import net.usikkert.kouchat.ui.ChatWindow; import net.usikkert.kouchat.ui.UserInterface; import net.usikkert.kouchat.util.Sleeper; import net.usikkert.kouchat.util.Tools; import net.usikkert.kouchat.util.Validate; import android.content.Context; import android.os.AsyncTask; import android.widget.Toast; /** * Implementation of a KouChat user interface that communicates with the Android GUI. * * @author Christian Ihle */ public class AndroidUserInterface implements UserInterface, ChatWindow { private final MessageController msgController; private final Controller controller; private final UserList userList; private final User me; private final MessageStylerWithHistory messageStyler; private final Context context; private final AndroidSettings settings; private final NotificationService notificationService; private final CommandParser commandParser; private final TransferList transferList; private final AndroidFileUtils androidFileUtils; private final Sleeper sleeper; private final ErrorHandler errorHandler; private MainChatController mainChatController; public AndroidUserInterface(final Context context, final AndroidSettings settings, final NotificationService notificationService) { Validate.notNull(context, "Context can not be null"); Validate.notNull(settings, "Settings can not be null"); Validate.notNull(notificationService, "NotificationService can not be null"); this.context = context; this.settings = settings; this.notificationService = notificationService; this.errorHandler = new ErrorHandler(); // TODO add a listener for showing errors messageStyler = new MessageStylerWithHistory(context); msgController = new MessageController(this, this, settings, errorHandler); final CoreMessages coreMessages = new CoreMessages(); controller = new Controller(this, settings, new AndroidSettingsSaver(), coreMessages, errorHandler); commandParser = new CommandParser(controller, this, settings, coreMessages); androidFileUtils = new AndroidFileUtils(); sleeper = new Sleeper(); userList = controller.getUserList(); transferList = controller.getTransferList(); me = settings.getMe(); } /** * Just accepts the file transfer for now. A file receiver will be added, and * {@link #showFileSave(FileReceiver)} will be called afterwards. */ @Override public boolean askFileSave(final String user, final String fileName, final String size) { return true; } /** * Uses a notification to ask the user to accept or reject this file transfer request. * The notification is canceled afterwards. * * <p>Expects this to be called from a different thread, as it waits for an answer * before it continues execution.</p> * * @param fileReceiver Information about the file to save. */ @Override public void showFileSave(final FileReceiver fileReceiver) { notificationService.notifyNewFileTransfer(fileReceiver); while (!fileReceiver.isAccepted() && !fileReceiver.isRejected() && !fileReceiver.isCanceled()) { sleeper.sleep(500); } if (!fileReceiver.isAccepted()) { notificationService.cancelFileTransferNotification(fileReceiver); } } /** * Registers a listener for receiving files, and makes sure the file is stored in the downloads directory * with a unique name. * * @param fileRes The file reception object. */ @Override public void showTransfer(final FileReceiver fileRes) { Validate.notNull(fileRes, "FileReceiver can not be null"); final File fileInDownloads = androidFileUtils.createFileInDownloadsWithAvailableName(fileRes.getFileName()); fileRes.setFile(fileInDownloads); new AndroidFileTransferListener(fileRes, context, androidFileUtils, msgController, notificationService); } /** * Registers a listener for sending files. * * @param fileSend The file sending object. */ @Override public void showTransfer(final FileSender fileSend) { Validate.notNull(fileSend, "FileSender can not be null"); new AndroidFileTransferListener(fileSend, context, androidFileUtils, msgController, notificationService); } /** * Gets a file receiver object for the given user with the given file transfer id. * * @param userCode The unique code of the user who requests to send a file. * @param fileTransferId The id of the request to send a file. * @return The file transfer object, if found, or <code>null</code> if not found. */ public FileReceiver getFileReceiver(final int userCode, final int fileTransferId) { final User user = getUser(userCode); return transferList.getFileReceiver(user, fileTransferId); } public void cancelFileTransfer(final int userCode, final int fileTransferId) { final User user = getUser(userCode); final FileTransfer fileTransfer = transferList.getFileTransfer(user, fileTransferId); if (fileTransfer != null) { commandParser.cancelFileTransfer(fileTransfer); } } /** * Updates the title and topic of the main chat. * * <p>If there is no topic, then <code>null</code> is sent as the topic, to hide line number two.</p> * <p>If the user is away, then (Away) is included in the title.</p> * <p>If the network connection is down, connection details are shown instead.</p> * * <p>Looks like this:</p> * <pre> * <code>Nick name (Away) - KouChat</code> * <code>The topic - Nick name of the user that set the topic</code> * </pre> * * <p>Example:</p> * <pre> * <code>Penny (Away) - KouChat</code> * <code>Knock knock - Sheldon</code> * </pre> * * <p>Or just:</p> * <pre> * <code>Penny - KouChat</code> * </pre> * * <p>If never connected:</p> * <pre> * <code>Not connected - KouChat</code> * </pre> * * <p>If the connection has been lost, after having been connected earlier:</p> * <pre> * <code>Connection lost - KouChat</code> * </pre> */ @Override public void showTopic() { if (mainChatController != null) { final String title = formatTitle(); final String topic = formatTopic(); mainChatController.updateTitleAndSubtitle(title, topic); } } private String formatTitle() { if (!controller.isConnected()) { if (controller.isLoggedOn()) { return me.getNick() + " - Connection lost - " + Constants.APP_NAME; } else { return me.getNick() + " - Not connected - " + Constants.APP_NAME; } } else if (isAway()) { return me.getNick() + " (Away) - " + Constants.APP_NAME; } return me.getNick() + " - " + Constants.APP_NAME; } private String formatTopic() { if (!controller.isConnected()) { return null; } final Topic topic = controller.getTopic(); if (topic.hasTopic()) { return topic.getTopic() + " - " + topic.getNick(); } else { return null; } } /** * Gets the current topic. * * @return The current topic. */ public String getTopic() { return controller.getTopic().getTopic(); } /** * Changes the topic to the specified topic. Set to empty to remove the topic. * * @param topic The new topic to set. */ public void changeTopic(final String topic) { new CommandWithToastOnExceptionAsyncTask(context, new Command() { @Override public void runCommand() throws CommandException { commandParser.fixTopic(topic); } }).execute(); } @Override public void clearChat() { } /** * Updates the title and topic when away mode is changed. * * @param away If the user is away. */ @Override public void changeAway(final boolean away) { showTopic(); } /** * Checks if the application user is currently away. * * @return If away. */ public boolean isAway() { return me.isAway(); } /** * Sets the application user as away, with the specified away message. * * @param awayMessage The away message to use. */ public void goAway(final String awayMessage) { new CommandWithToastOnExceptionAsyncTask(context, new Command() { @Override public void runCommand() throws CommandException { controller.goAway(awayMessage); } }).execute(); } /** * Sets the application user as back from away. */ public void comeBack() { new CommandWithToastOnExceptionAsyncTask(context, new Command() { @Override public void runCommand() throws CommandException { controller.comeBack(); } }).execute(); } /** * Notifies about a new message if the main chat is not visible. */ @Override public void notifyMessageArrived(final User user, final String message) { if (!isVisible()) { notificationService.notifyNewMainChatMessage(user, message); } } /** * Notifies about a new message if neither the main chat nor the private chat with * the user who sent the message is visible. */ @Override public void notifyPrivateMessageArrived(final User user, final String message) { Validate.notNull(user, "User can not be null"); Validate.notNull(user.getPrivchat(), "Private chat can not be null"); if (!isVisible() && !user.getPrivchat().isVisible()) { notificationService.notifyNewPrivateChatMessage(user, message); } } @Override public MessageController getMessageController() { return msgController; } @Override public void createPrivChat(final User user) { Validate.notNull(user, "User can not be null"); if (user.getPrivchat() == null) { user.setPrivchat(new AndroidPrivateChatWindow(context, user)); } if (user.getPrivateChatLogger() == null) { user.setPrivateChatLogger(new ChatLogger(user.getNick(), settings, errorHandler)); } } @Override public boolean isVisible() { return mainChatController != null && mainChatController.isVisible(); } @Override public boolean isFocused() { return isVisible(); } @Override public void quit() { } @Override public void appendToChat(final String message, final int color) { Validate.notEmpty(message, "Message can not be empty"); final CharSequence styledMessage = messageStyler.styleAndAppend(message, color); if (mainChatController != null) { mainChatController.appendToChat(styledMessage); } } public void logOn() { controller.start(); controller.logOn(); } public void logOff() { final AsyncTask<Void, Void, Void> logOffTask = new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(final Void... voids) { controller.logOff(false); controller.shutdown(); return null; } }; logOffTask.execute(); } public void registerMainChatController(final MainChatController theMainChatController) { Validate.notNull(theMainChatController, "MainChatController can not be null"); mainChatController = theMainChatController; mainChatController.updateChat(messageStyler.getHistory()); } public void unregisterMainChatController(final MainChatController theMainChatController) { if (this.mainChatController == theMainChatController) { this.mainChatController = null; } } public void sendMessage(final String message) { Validate.notEmpty(message, "Message can not be empty"); final AsyncTask<Void, Void, Void> sendMessageTask = new AsyncTask<Void, Void, Void>() { @Override public Void doInBackground(final Void... voids) { try { controller.sendChatMessage(message); msgController.showOwnMessage(message); } catch (final CommandException e) { msgController.showSystemMessage(e.getMessage()); } return null; } }; sendMessageTask.execute(); } public void sendPrivateMessage(final String privateMessage, final User user) { Validate.notEmpty(privateMessage, "Private message can not be empty"); Validate.notNull(user, "User can not be null"); final AsyncTask<Void, Void, Void> sendMessageTask = new AsyncTask<Void, Void, Void>() { @Override public Void doInBackground(final Void... voids) { try { controller.sendPrivateMessage(privateMessage, user); msgController.showPrivateOwnMessage(user, privateMessage); } catch (final CommandException e) { msgController.showPrivateSystemMessage(user, e.getMessage()); } return null; } }; sendMessageTask.execute(); } public boolean changeNickName(final String nick) { final String trimNick = nick.trim(); if (trimNick.equals(me.getNick())) { return false; } if (!Tools.isValidNick(trimNick)) { Toast.makeText(context, context.getString(R.string.error_nick_name_invalid), Toast.LENGTH_LONG).show(); } else if (controller.isNickInUse(trimNick)) { Toast.makeText(context, R.string.error_nick_name_in_use, Toast.LENGTH_LONG).show(); } else { return doChangeNickName(trimNick); } return false; } private boolean doChangeNickName(final String nick) { final CommandWithToastOnExceptionAsyncTask changeNickNameTask = new CommandWithToastOnExceptionAsyncTask(context, new Command() { @Override public void runCommand() throws CommandException { controller.changeMyNick(nick); msgController.showSystemMessage(context.getString(R.string.message_your_nick_name_changed, me.getNick())); showTopic(); } }); changeNickNameTask.execute(); try { return changeNickNameTask.get(); } // Not sure if anything is going to interrupt this thread in practice catch (final InterruptedException e) { throw new RuntimeException(String.format("Was interrupted while changing nick name to '%s'", nick), e); } // Happens if the async task throws any exceptions catch (final ExecutionException e) { throw new RuntimeException(String.format("Something went wrong changing nick name to '%s'", nick), e); } } /** * Gets a user with the specified user code from the controller. * * @param code The unique code of the user to get. * @return The user who was found, or <code>null</code>. */ public User getUser(final int code) { return controller.getUser(code); } /** * Resets the new private message field of the user, in addition to the current notification for the user. * * @param user The user who's private chat got activated. */ public void activatedPrivChat(final User user) { if (user.isNewPrivMsg()) { user.setNewPrivMsg(false); // In case the user has logged off controller.changeNewMessage(user.getCode(), false); } notificationService.resetPrivateChatNotification(user); } /** * Updates whether the user is currently writing or not. This makes sure a star is shown * by the nick name in the user list, and sends a notice to other users so they can show the same thing. * * @param isCurrentlyWriting If the application user is currently writing. */ public void updateMeWriting(final boolean isCurrentlyWriting) { final AsyncTask<Void, Void, Void> updateMeWritingTask = new AsyncTask<Void, Void, Void>() { @Override public Void doInBackground(final Void... voids) { controller.updateMeWriting(isCurrentlyWriting); return null; } }; updateMeWritingTask.execute(); } /** * Resets both private and main chat notifications using the notification service. */ public void resetAllMessageNotifications() { notificationService.resetAllMessageNotifications(); } /** * Gets the backend user list. * * @return The user list. */ public UserList getUserList() { return userList; } /** * Gets the application user. * * @return Me. */ public User getMe() { return me; } /** * Gets the application settings. * * @return The settings. */ public AndroidSettings getSettings() { return settings; } /** * Sends the file to the specified user. Shows a Toast if it fails. * * @param user The user to send to. * @param file The file to send. */ public void sendFile(final User user, final FileToSend file) { new CommandWithToastOnExceptionAsyncTask(context, new Command() { @Override public void runCommand() throws CommandException { commandParser.sendFile(user, file); } }).execute(); } public void registerNetworkConnectionListener(final NetworkConnectionListener listener) { controller.registerNetworkConnectionListener(listener); } }