/** * */ package net.frontlinesms.ui.handler.phones; import static net.frontlinesms.FrontlineSMSConstants.MESSAGE_MODEM_LIST_UPDATED; import static net.frontlinesms.ui.UiGeneratorControllerConstants.TAB_ADVANCED_PHONE_MANAGER; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.concurrent.CopyOnWriteArraySet; import net.frontlinesms.FrontlineSMSConstants; import net.frontlinesms.data.domain.EmailAccount; import net.frontlinesms.data.domain.SmsInternetServiceSettings; import net.frontlinesms.data.domain.SmsModemSettings; import net.frontlinesms.data.events.DatabaseEntityNotification; import net.frontlinesms.data.repository.SmsModemSettingsDao; import net.frontlinesms.events.EventObserver; import net.frontlinesms.events.FrontlineEventNotification; import net.frontlinesms.messaging.FrontlineMessagingService; import net.frontlinesms.messaging.FrontlineMessagingServiceStatus; import net.frontlinesms.messaging.FrontlineMessagingServiceEventListener; import net.frontlinesms.messaging.mms.MmsService; import net.frontlinesms.messaging.mms.MmsServiceManager; import net.frontlinesms.messaging.mms.email.MmsEmailService; import net.frontlinesms.messaging.mms.email.MmsEmailServiceStatus; import net.frontlinesms.messaging.mms.events.MmsServiceStatusNotification; import net.frontlinesms.messaging.sms.SmsService; import net.frontlinesms.messaging.sms.SmsServiceManager; import net.frontlinesms.messaging.sms.internet.SmsInternetService; import net.frontlinesms.messaging.sms.internet.SmsInternetServiceStatus; import net.frontlinesms.messaging.sms.modem.SmsModem; import net.frontlinesms.messaging.sms.modem.SmsModemStatus; import net.frontlinesms.ui.Event; import net.frontlinesms.ui.Icon; import net.frontlinesms.ui.UiDestroyEvent; import net.frontlinesms.ui.UiGeneratorController; import net.frontlinesms.ui.events.FrontlineUiUpateJob; import net.frontlinesms.ui.events.TabChangedNotification; import net.frontlinesms.ui.handler.BaseTabHandler; import net.frontlinesms.ui.handler.email.EmailAccountSettingsDialogHandler; import net.frontlinesms.ui.handler.settings.SmsInternetServiceSettingsHandler; import net.frontlinesms.ui.i18n.InternationalisationUtils; import net.frontlinesms.ui.i18n.TextResourceKeyOwner; import serial.NoSuchPortException; /** * Event handler for the Phones tab and associated dialogs * @author Alex Anderson <alex@frontlinesms.com> * @author Morgan Belkadi <morgan@frontlinesms.com> */ @TextResourceKeyOwner(prefix={"COMMON_", "I18N_", "MESSAGE_"}) public class PhoneTabHandler extends BaseTabHandler implements FrontlineMessagingServiceEventListener, EventObserver { //> STATIC CONSTANTS /** {@link Comparator} used for sorting {@link FrontlineMessagingService}s into a friendly order. */ private static final Comparator<? super FrontlineMessagingService> MESSAGING_SERVICE_COMPARATOR = new Comparator<FrontlineMessagingService>() { public int compare(FrontlineMessagingService one, FrontlineMessagingService tother) { int comparison = 0; // Always Modems first, then Internet Services, then MMS Services if (one instanceof SmsModem) { if (tother instanceof SmsModem) { comparison = ((SmsModem)one).getPort().compareTo(((SmsModem)tother).getPort()); } else { comparison = -1; } } else if (one.getClass().equals(tother.getClass())) { comparison = one.getServiceName().compareTo(tother.getServiceName()); } else if (one instanceof SmsInternetService) { comparison = (tother instanceof SmsModem ? 1 : -1); } return comparison; }}; //> THINLET UI LAYOUT FILES /** UI XML File Path: the Phones Tab itself */ private static final String UI_FILE_PHONES_TAB = "/ui/core/phones/phonesTab.xml"; //> I18n TEXT KEYS /** I18n Text Key: TODO */ private static final String COMMON_PHONE_CONNECTED = "common.phone.connected"; /** I18n Text Key: TODO */ private static final String COMMON_SMS_INTERNET_SERVICE_CONNECTED = "common.sms.internet.service.connected"; /** I18n Text Key: Last checked: %0. */ private static final String I18N_EMAIL_LAST_CHECKED = "email.last.checked"; //> THINLET UI COMPONENT NAMES /** UI Component name: TODO */ private static final String COMPONENT_PHONE_MANAGER_MODEM_LIST = "phoneManager_modemList"; /** UI Component name: TODO */ private static final String COMPONENT_PHONE_MANAGER_MODEM_LIST_ERROR = "phoneManager_modemListError"; //> INSTANCE PROPERTIES /** The manager of {@link FrontlineMessagingService}s */ private final SmsServiceManager phoneManager; /** Data Access Object for {@link SmsModemSettings}s */ private final SmsModemSettingsDao smsModelSettingsDao; private MmsServiceManager mmsServiceManager; //> CONSTRUCTORS /** * Create a new instance of this class. * @param uiController value for {@link #ui} */ public PhoneTabHandler(UiGeneratorController ui) { super(ui); this.phoneManager = ui.getPhoneManager(); this.mmsServiceManager = ui.getFrontlineController().getMmsServiceManager(); this.smsModelSettingsDao = ui.getPhoneDetailsManager(); } @Override protected Object initialiseTab() { // We register the observer to the UIGeneratorController, which notifies when tabs have changed this.ui.getFrontlineController().getEventBus().registerObserver(this); return ui.loadComponentFromFile(UI_FILE_PHONES_TAB, this); } //> ACCESSORS /** @return the compoenent containing the list of connected devices */ private Object getModemListComponent() { return ui.find(this.getTab(), COMPONENT_PHONE_MANAGER_MODEM_LIST); } //> THINLET UI METHODS /** * Event fired when the view phone details action is fired. * @param list the thinlet UI list containing details of the devices */ public void showPhoneSettingsDialog(Object list) { Object selected = ui.getSelectedItem(list); if (selected != null) { FrontlineMessagingService service = ui.getAttachedObject(selected, FrontlineMessagingService.class); if (service instanceof SmsModem) { SmsModem modem = (SmsModem) service; if (modem.isConnected()) { showPhoneSettingsDialog(modem, false); } else { showPhoneConfigDialog(list); } } else if (service instanceof SmsInternetService) { SmsInternetServiceSettingsHandler serviceHandler = new SmsInternetServiceSettingsHandler(this.ui); serviceHandler.showConfigureService((SmsInternetService) service, null); } else if (service instanceof MmsEmailService) { EmailAccountSettingsDialogHandler emailAccountSettingsDialogHandler = new EmailAccountSettingsDialogHandler(ui, true); emailAccountSettingsDialogHandler.initDialog(((MmsEmailService) service).getEmailAccount()); this.ui.add(emailAccountSettingsDialogHandler.getDialog()); } } } /** * Event fired when the view phone details action is chosen. * @param device The device we are showing settings for * @param isNewPhone <code>true</code> TODO <if this phone has previously connected (i.e. not first time it has connected)> OR <if this phone has just connected (e.g. may have connected before, but not today)> */ public void showPhoneSettingsDialog(SmsModem device, boolean isNewPhone) { final DeviceSettingsDialogHandler deviceSettingsDialog = new DeviceSettingsDialogHandler(ui, device, isNewPhone); new FrontlineUiUpateJob() { public void run() { deviceSettingsDialog.initDialog(); ui.add(deviceSettingsDialog.getDialog()); } }.execute(); } /** * Enables or disables the <b>Edit Phone Settings</b>. * <br>If the supplied list is not empty, then the option is enabled. Otherwise, it is disabled. * TODO would be good to know when this event is triggered * @param list * @param menuItem */ public void editPhoneEnabled(Object list, Object menuItem) { ui.setVisible(menuItem, ui.getSelectedItem(list) != null); } /** * Disconnect from a specific {@link FrontlineMessagingService}. * @param list The list of connected {@link FrontlineMessagingService}s in the Phones tab. */ public void disconnectFromSelected(Object list) { Object selected = ui.getSelectedItem(list); if (selected != null) { FrontlineMessagingService service = ui.getAttachedObject(selected, FrontlineMessagingService.class); if (service instanceof SmsService) { phoneManager.disconnect((SmsService) service); } else if (service instanceof MmsEmailService) { this.mmsServiceManager.connectMmsEmailService((MmsEmailService) service, false); } refresh(); } } /** * Stop detection of the {@link FrontlineMessagingService} on a specific port. * @param list The list of ports which are currently being probed for connected {@link FrontlineMessagingService}s. */ public void stopDetection(Object list) { FrontlineMessagingService dev = ui.getAttachedObject(ui.getSelectedItem(list), FrontlineMessagingService.class); if (dev instanceof SmsModem) { SmsModem modem = (SmsModem) dev; phoneManager.stopDetection(modem.getPort()); } refresh(); } /** * Action triggered when an item in the unconnected phones/ports list is selected. This method * enables and disables relevant items in the contextual popupmenu for the selected port. * @param popUp * @param list */ public void phoneManager_enabledFields(Object popUp, Object list) { Object selected = ui.getSelectedItem(list); if (selected == null) { ui.setVisible(popUp, false); } else { FrontlineMessagingService service = ui.getAttachedObject(selected, FrontlineMessagingService.class); if (service instanceof SmsModem) { SmsModem modem = (SmsModem) service; ui.setVisible(ui.find(popUp, "miEditPhone"), false); ui.setVisible(ui.find(popUp, "miAutoConnect"), !modem.isDetecting() && !modem.isTryToConnect()); ui.setVisible(ui.find(popUp, "miManualConnection"), !modem.isDetecting() && !modem.isTryToConnect()); ui.setVisible(ui.find(popUp, "miCancelDetection"), modem.isDetecting()); } else { for (Object o : ui.getItems(popUp)) { ui.setVisible(o, ui.getName(o).equals("miEditPhone") || ui.getName(o).equals("miAutoConnect")); } } } } /** * Attempt to automatically connect to the service currently selected in the list of * unconnected services. */ public void connectToSelectedPhoneHandler() { Object modemListError = ui.find(COMPONENT_PHONE_MANAGER_MODEM_LIST_ERROR); Object selected = ui.getSelectedItem(modemListError); if (selected != null) { FrontlineMessagingService service = ui.getAttachedObject(selected, FrontlineMessagingService.class); if (service instanceof SmsModem) { SmsModem modem = (SmsModem) service; try { phoneManager.requestConnect(modem.getPort()); } catch (NoSuchPortException ex) { log.info("", ex); } } else if (service instanceof SmsInternetService) { phoneManager.addSmsInternetService((SmsInternetService) service); } else if (service instanceof MmsEmailService) { this.mmsServiceManager.connectMmsEmailService((MmsEmailService) service, true); } } } /** * Show the dialog for connecting a phone with manual configuration. * @param list TODO what is this list, and why is it necessary? Could surely just find it * TODO FIXME XXX need to be much clearer about when this method should be available. */ public void showPhoneConfigDialog(Object list) { Object selected = ui.getSelectedItem(list); // This assumes that the attached FrontlineMessagingService is an instance of SmsModem final SmsModem selectedModem = ui.getAttachedObject(selected, SmsModem.class); // We create the manual config dialog and put the display job in the AWT event queue final DeviceManualConfigDialogHandler configDialog = new DeviceManualConfigDialogHandler(ui, selectedModem); new FrontlineUiUpateJob() { public void run() { ui.add(configDialog.getDialog()); } }.execute(); } /** Starts the phone auto-detector. */ public void phoneManager_detectModems() { phoneManager.refreshPhoneList(true); } /** * called when one of the SMS devices (phones or http senders) has a change in status, * such as detection, connection, disconnecting, running out of batteries, etc. * see PhoneHandler.STATUS_CODE_MESSAGES[smsDeviceEventCode] to get the relevant messages * @param messagingService * @param serviceStatus */ public void messagingServiceEvent(FrontlineMessagingService messagingService, FrontlineMessagingServiceStatus serviceStatus) { log.trace("ENTER"); // Handle modems first if (messagingService instanceof SmsModem) { SmsModem activeService = (SmsModem) messagingService; // FIXME re-implement status update on the AWT Event Queue - doing it here causes splitpanes to collapse // ui.setStatus(InternationalisationUtils.getI18NString(MESSAGE_PHONE) + ": " + activeDevice.getPort() + ' ' + getSmsDeviceStatusAsString(device)); if (serviceStatus.equals(SmsModemStatus.CONNECTED)) { log.debug("Phone is connected. Try to read details from database!"); String serial = activeService.getSerial(); SmsModemSettings settings = smsModelSettingsDao.getSmsModemSettings(serial); // If this is the first time we've attached this phone, or no settings were // saved last time, we should show the settings dialog automatically if(settings == null) { log.debug("User need to make setting related this phone."); showPhoneSettingsDialog(activeService, true); } else { // Let's update the Manufacturer & Model for this device, if it wasn't previously set if (settings.getManufacturer() == null || settings.getModel() == null) { settings.setManufacturer(activeService.getManufacturer()); settings.setModel(activeService.getModel()); smsModelSettingsDao.updateSmsModemSettings(settings); } boolean supportsReceive = activeService.supportsReceive(); if (settings.supportsReceive() != supportsReceive) { settings.setSupportsReceive(supportsReceive); smsModelSettingsDao.updateSmsModemSettings(settings); } activeService.setUseForSending(settings.useForSending()); activeService.setUseDeliveryReports(settings.useDeliveryReports()); if(activeService.supportsReceive()) { activeService.setUseForReceiving(settings.useForReceiving()); activeService.setDeleteMessagesAfterReceiving(settings.deleteMessagesAfterReceiving()); } } ui.newEvent(new Event(Event.TYPE_PHONE_CONNECTED, InternationalisationUtils.getI18nString(COMMON_PHONE_CONNECTED) + ": " + activeService.getModel())); } } else { SmsInternetService service = (SmsInternetService) messagingService; // TODO document why newEvent is called here, and why it is only called for certain statuses. if (serviceStatus.equals(SmsInternetServiceStatus.CONNECTED)) { ui.newEvent(new Event( Event.TYPE_SMS_INTERNET_SERVICE_CONNECTED, InternationalisationUtils.getI18nString(COMMON_SMS_INTERNET_SERVICE_CONNECTED) + ": " + SmsInternetServiceSettingsHandler.getProviderName(service.getClass()) + " - " + service.getIdentifier())); } else if (serviceStatus.equals(SmsInternetServiceStatus.RECEIVING_FAILED)) { ui.newEvent(new Event( Event.TYPE_SMS_INTERNET_SERVICE_RECEIVING_FAILED, SmsInternetServiceSettingsHandler.getProviderName(service.getClass()) + " - " + service.getIdentifier() + ": " + InternationalisationUtils.getI18nString(FrontlineSMSConstants.COMMON_SMS_INTERNET_SERVICE_RECEIVING_FAILED))); } } refresh(); log.trace("EXIT"); } //> INSTANCE HELPER METHODS /** * Refreshes the list of PhoneHandlers displayed on the PhoneManager tab. */ public void refresh() { new FrontlineUiUpateJob() { public void run() { Object modemListError = ui.find(COMPONENT_PHONE_MANAGER_MODEM_LIST_ERROR); // cache the selected item so we can reselect it when we've finished! int index = ui.getSelectedIndex(modemListError); int indexTop = ui.getSelectedIndex(getModemListComponent()); ui.removeAll(getModemListComponent()); ui.removeAll(modemListError); Collection<FrontlineMessagingService> messagingServices = new CopyOnWriteArraySet<FrontlineMessagingService>(); messagingServices.addAll(phoneManager.getAll()); messagingServices.addAll(mmsServiceManager.getAll()); FrontlineMessagingService[] messagingServicesArray = messagingServices.toArray(new FrontlineMessagingService[0]); Arrays.sort(messagingServicesArray, MESSAGING_SERVICE_COMPARATOR); for (FrontlineMessagingService messagingService : messagingServicesArray) { if (messagingService.isConnected()) { ui.add(getModemListComponent(), getTableRow(messagingService, true)); } else { ui.add(modemListError, getTableRow(messagingService, false)); } } ui.setSelectedIndex(getModemListComponent(), indexTop); ui.setSelectedIndex(modemListError, index); ui.updateActiveConnections(); } }.execute(); } private Object getTableRow(FrontlineMessagingService service, boolean isConnected) { Object row = ui.createTableRow(service); this.ui.setAttachedObject(row, service); /** TYPE CELL (Icon) */ final String typeIcon; if (service instanceof SmsModem) { typeIcon = Icon.PHONE_NUMBER; } else { typeIcon = Icon.SMS_HTTP; } Object typeCell = ui.createTableCell(""); ui.setIcon(typeCell, typeIcon); ui.add(row, typeCell); /** MESSAGE TYPE CELL (Icon) */ Object messageTypeCell = ui.createTableCell(""); if (service instanceof MmsService) { ui.setIcon(messageTypeCell, Icon.MMS); } else if (service.isConnected() || service instanceof SmsInternetService) { ui.setIcon(messageTypeCell, Icon.SMS); } ui.add(row, messageTypeCell); /** PORT CELL */ Object portCell = ui.createTableCell(service.getDisplayPort()); ui.add(row, portCell); /** NAME CELL */ Object nameCell = ui.createTableCell(service.getServiceName()); ui.add(row, nameCell); /** ID CELL */ Object idCell = ui.createTableCell(service.getServiceIdentification()); ui.add(row, idCell); /** USE FOR RECEIVING/SENDING CELLS */ Object useForSendingCell = ui.createTableCell(""); Object useForReceiveCell = ui.createTableCell(""); if (isConnected) { if (service.isUseForSending()) ui.setIcon(useForSendingCell, Icon.CIRLCE_TICK); if (service.isUseForReceiving()) ui.setIcon(useForReceiveCell, Icon.CIRLCE_TICK); ui.add(row, useForSendingCell); ui.add(row, useForReceiveCell); } /** STATUS CELL */ // Add "status cell" - "traffic light" showing how functional the device is final String statusIcon; FrontlineMessagingServiceStatus status = service.getStatus(); if (status.equals(SmsModemStatus.CONNECTING) || status.equals(SmsModemStatus.DETECTED) || status.equals(SmsModemStatus.TRY_TO_CONNECT) || status.equals(MmsEmailServiceStatus.FETCHING)) { statusIcon = Icon.LED_AMBER; } else if (service.isConnected()){ statusIcon = Icon.LED_GREEN; } else { statusIcon = Icon.LED_RED; } Object statusCell = ui.createTableCell(getServiceStatusAsString(service)); ui.setIcon(statusCell, statusIcon); ui.add(row, statusCell); return row; } //> STATIC FACTORIES //> STATIC HELPER METHODS /** * Gets the status of an {@link FrontlineMessagingService} as an internationalised {@link String}. * @param service * @return An internationalised {@link String} describing the status of the {@link FrontlineMessagingService}. */ private static String getServiceStatusAsString(FrontlineMessagingService service) { String statusString = InternationalisationUtils.getI18nString(service.getStatus().getI18nKey(), service.getStatusDetail()); if (service instanceof MmsEmailService && service.getStatus().equals(MmsEmailServiceStatus.READY)) { Long lastChecked = ((MmsEmailService) service).getEmailAccount().getLastCheck(); if (lastChecked != null) { statusString += " - " + InternationalisationUtils.getI18nString(I18N_EMAIL_LAST_CHECKED, InternationalisationUtils.getDatetimeFormat().format(new Date (lastChecked))); } } return statusString; } /** * UI event called when the user changes tab */ public void notify(FrontlineEventNotification notification) { // This object is registered to the UIGeneratorController and get notified when the users changes tab if (notification instanceof TabChangedNotification) { String newTabName = ((TabChangedNotification) notification).getNewTabName(); if (newTabName.equals(TAB_ADVANCED_PHONE_MANAGER)) { this.refresh(); this.ui.setStatus(InternationalisationUtils.getI18nString(MESSAGE_MODEM_LIST_UPDATED)); } } else if (notification instanceof MmsServiceStatusNotification) { refresh(); } else if (notification instanceof DatabaseEntityNotification<?>) { // Database notification Object entity = ((DatabaseEntityNotification<?>) notification).getDatabaseEntity(); if (entity instanceof EmailAccount || entity instanceof SmsModemSettings || entity instanceof SmsInternetServiceSettings) { // If there is any change in the E-Mail accounts, we refresh the list of Messaging Services refresh(); } } else if (notification instanceof UiDestroyEvent) { if(((UiDestroyEvent) notification).isFor(this.ui)) { this.ui.getFrontlineController().getEventBus().unregisterObserver(this); } } } }