From 721c391a2bcd6acc200f8beac6677ee743bb2194 Mon Sep 17 00:00:00 2001 From: Brian West Date: Mon, 4 Jan 2010 20:49:22 +0000 Subject: [PATCH] Let there be PHONE CALLS! git-svn-id: http://svn.freeswitch.org/svn/freeswitch/trunk@16142 d0543943-73ff-0310-b7d9-9358b9ac24b2 --- fscomm/FSPhone.pro | 40 +++ fscomm/call.cpp | 43 +++ fscomm/call.h | 73 +++++ fscomm/conf/accounts/example.xml | 34 +++ fscomm/conf/event_socket.conf.xml | 8 + fscomm/conf/freeswitch.serial | Bin 0 -> 13 bytes fscomm/conf/freeswitch.xml | 267 ++++++++++++++++++ fscomm/conf/portaudio.conf.xml | 17 ++ fscomm/fshost.cpp | 352 +++++++++++++++++++++++ fscomm/fshost.h | 106 +++++++ fscomm/main.cpp | 51 ++++ fscomm/mainwindow.cpp | 334 ++++++++++++++++++++++ fscomm/mainwindow.h | 80 ++++++ fscomm/mainwindow.ui | 350 +++++++++++++++++++++++ fscomm/mod_qsettings/mod_qsettings.cpp | 149 ++++++++++ fscomm/mod_qsettings/mod_qsettings.h | 51 ++++ fscomm/prefdialog.cpp | 132 +++++++++ fscomm/prefdialog.h | 27 ++ fscomm/prefdialog.ui | 376 +++++++++++++++++++++++++ fscomm/resources.qrc | 13 + fscomm/resources/pref_audio.gif | Bin 0 -> 32099 bytes fscomm/resources/pref_sip.png | Bin 0 -> 1987 bytes fscomm/resources/splash.png | Bin 0 -> 6418 bytes 23 files changed, 2503 insertions(+) create mode 100644 fscomm/FSPhone.pro create mode 100644 fscomm/call.cpp create mode 100644 fscomm/call.h create mode 100644 fscomm/conf/accounts/example.xml create mode 100644 fscomm/conf/event_socket.conf.xml create mode 100644 fscomm/conf/freeswitch.serial create mode 100644 fscomm/conf/freeswitch.xml create mode 100644 fscomm/conf/portaudio.conf.xml create mode 100644 fscomm/fshost.cpp create mode 100644 fscomm/fshost.h create mode 100644 fscomm/main.cpp create mode 100644 fscomm/mainwindow.cpp create mode 100644 fscomm/mainwindow.h create mode 100644 fscomm/mainwindow.ui create mode 100644 fscomm/mod_qsettings/mod_qsettings.cpp create mode 100644 fscomm/mod_qsettings/mod_qsettings.h create mode 100644 fscomm/prefdialog.cpp create mode 100644 fscomm/prefdialog.h create mode 100644 fscomm/prefdialog.ui create mode 100644 fscomm/resources.qrc create mode 100644 fscomm/resources/pref_audio.gif create mode 100644 fscomm/resources/pref_sip.png create mode 100644 fscomm/resources/splash.png diff --git a/fscomm/FSPhone.pro b/fscomm/FSPhone.pro new file mode 100644 index 0000000000..666c7bbd09 --- /dev/null +++ b/fscomm/FSPhone.pro @@ -0,0 +1,40 @@ +# ##################################### +# version check qt +# ##################################### +contains(QT_VERSION, ^4\.[0-5]\..*) { + message("Cannot build FsGui with Qt version $$QT_VERSION.") + error("Use at least Qt 4.6.") +} +QT += xml +TARGET = fsphone +macx:TARGET = FSPhone +TEMPLATE = app +INCLUDEPATH = ../../../src/include \ + ../../../libs/apr/include \ + ../../../libs/libteletone/src +LIBS = -L../../../.libs \ + -lfreeswitch \ + -lm +!win32:!macx { + # This is here to comply with the default freeswitch installation + QMAKE_LFLAGS += -Wl,-rpath,/usr/local/freeswitch/lib + LIBS += -lcrypt \ + -lrt +} +SOURCES += main.cpp \ + mainwindow.cpp \ + fshost.cpp \ + call.cpp \ + mod_qsettings/mod_qsettings.cpp \ + prefdialog.cpp +HEADERS += mainwindow.h \ + fshost.h \ + call.h \ + mod_qsettings/mod_qsettings.h \ + prefdialog.h +FORMS += mainwindow.ui \ + prefdialog.ui +RESOURCES += resources.qrc +OTHER_FILES += conf/portaudio.conf.xml \ + conf/event_socket.conf.xml \ + conf/freeswitch.xml diff --git a/fscomm/call.cpp b/fscomm/call.cpp new file mode 100644 index 0000000000..6d16a3fc73 --- /dev/null +++ b/fscomm/call.cpp @@ -0,0 +1,43 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ + +#include "call.h" + +Call::Call() +{ +} + +Call::Call(int call_id, QString cid_name, QString cid_number, fsphone_call_direction_t direction, QString uuid) : + _call_id(call_id), + _cid_name(cid_name), + _cid_number(cid_number), + _direction(direction), + _uuid (uuid) +{ +} diff --git a/fscomm/call.h b/fscomm/call.h new file mode 100644 index 0000000000..3cde87416b --- /dev/null +++ b/fscomm/call.h @@ -0,0 +1,73 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ +#ifndef CALL_H +#define CALL_H + +#include +#include +#include + +typedef enum { + FSPHONE_CALL_STATE_RINGING = 0, + FSPHONE_CALL_STATE_TRYING = 1, + FSPHONE_CALL_STATE_ANSWERED = 2 +} fsphone_call_state_t; + +typedef enum { + FSPHONE_CALL_DIRECTION_INBOUND = 0, + FSPHONE_CALL_DIRECTION_OUTBOUND = 1 +} fsphone_call_direction_t; + +class Call +{ +public: + Call(void); + Call(int call_id, QString cid_name, QString cid_number, fsphone_call_direction_t direction, QString uuid); + QString getCidName(void) { return _cid_name; } + QString getCidNumber(void) { return _cid_number; } + int getCallID(void) { return _call_id; } + QString getUUID(void) { return _uuid; } + void setbUUID(QString uuid) { _buuid = uuid; } + fsphone_call_direction_t getDirection() { return _direction; } + fsphone_call_state_t getState() { return _state; } + void setState(fsphone_call_state_t state) { _state = state; } + +private: + int _call_id; + QString _cid_name; + QString _cid_number; + fsphone_call_direction_t _direction; + QString _uuid; + QString _buuid; + fsphone_call_state_t _state; +}; + +Q_DECLARE_METATYPE(Call) + +#endif // CALL_H diff --git a/fscomm/conf/accounts/example.xml b/fscomm/conf/accounts/example.xml new file mode 100644 index 0000000000..f8a682635f --- /dev/null +++ b/fscomm/conf/accounts/example.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fscomm/conf/event_socket.conf.xml b/fscomm/conf/event_socket.conf.xml new file mode 100644 index 0000000000..8f780ecf8f --- /dev/null +++ b/fscomm/conf/event_socket.conf.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fscomm/conf/freeswitch.serial b/fscomm/conf/freeswitch.serial new file mode 100644 index 0000000000000000000000000000000000000000..1ed544960745d0895c01207cc508c32b864f6fbd GIT binary patch literal 13 UcmYdFG&C|aur#qSPBLHs02d4b@&Et; literal 0 HcmV?d00001 diff --git a/fscomm/conf/freeswitch.xml b/fscomm/conf/freeswitch.xml new file mode 100644 index 0000000000..d7a382b136 --- /dev/null +++ b/fscomm/conf/freeswitch.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + diff --git a/fscomm/conf/portaudio.conf.xml b/fscomm/conf/portaudio.conf.xml new file mode 100644 index 0000000000..61a1f289d3 --- /dev/null +++ b/fscomm/conf/portaudio.conf.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/fscomm/fshost.cpp b/fscomm/fshost.cpp new file mode 100644 index 0000000000..d592c4485b --- /dev/null +++ b/fscomm/fshost.cpp @@ -0,0 +1,352 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ + +#include +#include "fshost.h" +#include "call.h" +#include "mod_qsettings/mod_qsettings.h" + +/* Declare it globally */ +FSHost g_FSHost; + +FSHost::FSHost(QObject *parent) : + QThread(parent) +{ + /* Initialize libs & globals */ + printf("Initializing globals...\n"); + switch_core_setrlimits(); + switch_core_set_globals(); + + qRegisterMetaType("Call"); + +} + +void FSHost::run(void) +{ + switch_core_flag_t flags = SCF_USE_SQL | SCF_USE_AUTO_NAT; + const char *err = NULL; + switch_bool_t console = SWITCH_FALSE; + switch_status_t destroy_status; + + /* Create directory structure for softphone with default configs */ + QDir conf_dir = QDir(QDir::home()); + if (!conf_dir.exists(".fsphone")) + { + conf_dir.mkpath(".fsphone/conf/accounts"); + conf_dir.mkpath(".fsphone/templates"); + QFile rootXML(":/confs/freeswitch.xml"); + QString dest = QString("%1/.fsphone/conf/freeswitch.xml").arg(conf_dir.absolutePath()); + rootXML.copy(dest); + + QFile defaultAccount(":/confs/example.xml"); + dest = QString("%1/.fsphone/conf/accounts/example.xml").arg(conf_dir.absolutePath()); + defaultAccount.copy(dest); + } + + /* Set all directories to the home user directory */ + if (conf_dir.cd(".fsphone")) + { + SWITCH_GLOBAL_dirs.conf_dir = (char *) malloc(strlen(QString("%1/conf").arg(conf_dir.absolutePath()).toAscii().constData()) + 1); + if (!SWITCH_GLOBAL_dirs.conf_dir) { + emit coreLoadingError("Cannot allocate memory for conf_dir."); + } + strcpy(SWITCH_GLOBAL_dirs.conf_dir, QString("%1/conf").arg(conf_dir.absolutePath()).toAscii().constData()); + + SWITCH_GLOBAL_dirs.log_dir = (char *) malloc(strlen(QString("%1/log").arg(conf_dir.absolutePath()).toAscii().constData()) + 1); + if (!SWITCH_GLOBAL_dirs.log_dir) { + emit coreLoadingError("Cannot allocate memory for log_dir."); + } + strcpy(SWITCH_GLOBAL_dirs.log_dir, QString("%1/log").arg(conf_dir.absolutePath()).toAscii().constData()); + + SWITCH_GLOBAL_dirs.run_dir = (char *) malloc(strlen(QString("%1/run").arg(conf_dir.absolutePath()).toAscii().constData()) + 1); + if (!SWITCH_GLOBAL_dirs.run_dir) { + emit coreLoadingError("Cannot allocate memory for run_dir."); + } + strcpy(SWITCH_GLOBAL_dirs.run_dir, QString("%1/run").arg(conf_dir.absolutePath()).toAscii().constData()); + + SWITCH_GLOBAL_dirs.db_dir = (char *) malloc(strlen(QString("%1/db").arg(conf_dir.absolutePath()).toAscii().constData()) + 1); + if (!SWITCH_GLOBAL_dirs.db_dir) { + emit coreLoadingError("Cannot allocate memory for db_dir."); + } + strcpy(SWITCH_GLOBAL_dirs.db_dir, QString("%1/db").arg(conf_dir.absolutePath()).toAscii().constData()); + + SWITCH_GLOBAL_dirs.script_dir = (char *) malloc(strlen(QString("%1/script").arg(conf_dir.absolutePath()).toAscii().constData()) + 1); + if (!SWITCH_GLOBAL_dirs.script_dir) { + emit coreLoadingError("Cannot allocate memory for script_dir."); + } + strcpy(SWITCH_GLOBAL_dirs.script_dir, QString("%1/script").arg(conf_dir.absolutePath()).toAscii().constData()); + + SWITCH_GLOBAL_dirs.htdocs_dir = (char *) malloc(strlen(QString("%1/htdocs").arg(conf_dir.absolutePath()).toAscii().constData()) + 1); + if (!SWITCH_GLOBAL_dirs.htdocs_dir) { + emit coreLoadingError("Cannot allocate memory for htdocs_dir."); + } + strcpy(SWITCH_GLOBAL_dirs.htdocs_dir, QString("%1/htdocs").arg(conf_dir.absolutePath()).toAscii().constData()); + } + + /* If you need to override configuration directories, you need to change them in the SWITCH_GLOBAL_dirs global structure */ + printf("Initializing core...\n"); + /* Initialize the core and load modules, that will startup FS completely */ + if (switch_core_init_and_modload(flags, console, &err) != SWITCH_STATUS_SUCCESS) { + fprintf(stderr, "Failed to initialize FreeSWITCH's core: %s\n", err); + emit coreLoadingError(err); + } + + printf("Everything OK, Entering runtime loop.\n"); + + if (switch_event_bind("FSHost", SWITCH_EVENT_ALL, SWITCH_EVENT_SUBCLASS_ANY, eventHandlerCallback, NULL) != SWITCH_STATUS_SUCCESS) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't bind!\n"); + } + + /* Load our QSettings module */ + if (switch_loadable_module_build_dynamic("mod_qsettings",mod_qsettings_load,NULL,mod_qsettings_shutdown,SWITCH_FALSE) != SWITCH_STATUS_SUCCESS) + { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't load mod_qsettings\n"); + } + QString res; + sendCmd("load", "mod_event_socket", &res); + emit ready(); + /* Go into the runtime loop. If the argument is true, this basically sets runtime.running = 1 and loops while that is set + * If its false, it initializes the libedit for the console, then does the same thing + */ + switch_core_runtime_loop(!console); + fflush(stdout); + + + switch_event_unbind_callback(eventHandlerCallback); + /* When the runtime loop exits, its time to shutdown */ + destroy_status = switch_core_destroy(); + if (destroy_status == SWITCH_STATUS_SUCCESS) + { + printf("We have properly shutdown the core.\n"); + } +} + +switch_status_t FSHost::processAlegEvent(switch_event_t * event, QString uuid) +{ + switch_status_t status = SWITCH_STATUS_SUCCESS; + Call * call = _active_calls.value(uuid); + /* Inbound call */ + if (call->getDirection() == FSPHONE_CALL_DIRECTION_INBOUND) + { + switch(event->event_id) { + case SWITCH_EVENT_CHANNEL_ANSWER: + { + call->setbUUID(switch_event_get_header_nil(event, "Other-Leg-Unique-ID")); + _bleg_uuids.insert(switch_event_get_header_nil(event, "Other-Leg-Unique-ID"), uuid); + call->setState(FSPHONE_CALL_STATE_ANSWERED); + emit answered(uuid); + break; + } + case SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE: + { + emit hungup(_active_calls.take(uuid)); + break; + } + case SWITCH_EVENT_CHANNEL_STATE: + { + printf("CHANNEL_STATE Answer-State: %s | Channel-State: %s | %s | %s\n", switch_event_get_header_nil(event, "Answer-State"),switch_event_get_header_nil(event, "Channel-State"), uuid.toAscii().constData(), switch_event_get_header_nil(event, "Other-Leg-Unique-ID")); + break; + } + default: + { + break; + } + } + } + /* Outbound call */ + else + { + switch(event->event_id) + { + case SWITCH_EVENT_CHANNEL_BRIDGE: + { + _active_calls.value(uuid)->setbUUID(switch_event_get_header_nil(event, "Other-Leg-Unique-ID")); + _bleg_uuids.insert(switch_event_get_header_nil(event, "Other-Leg-Unique-ID"), uuid); + break; + } + case SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE: + { + if (call->getState() == FSPHONE_CALL_STATE_TRYING) + { + emit callFailed(uuid); + _active_calls.take(uuid); + } + break; + } + default: + printf("A leg: %s(%s)\n",switch_event_name(event->event_id), switch_event_get_header_nil(event, "Event-Subclass")); + break; + } + } + return status; +} + +switch_status_t FSHost::processBlegEvent(switch_event_t * event, QString buuid) +{ + QString uuid = _bleg_uuids.value(buuid); + switch_status_t status = SWITCH_STATUS_SUCCESS; + Call * call = _active_calls.value(uuid); + /* Inbound call */ + if (call->getDirection() == FSPHONE_CALL_DIRECTION_INBOUND) + { + qDebug() << " Inbound call"; + } + /* Outbound call */ + else + { + switch(event->event_id) + { + case SWITCH_EVENT_CHANNEL_ANSWER: + { + emit answered(uuid); + break; + } + case SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE: + { + emit hungup(_active_calls.take(uuid)); + _bleg_uuids.take(buuid); + break; + } + case SWITCH_EVENT_CHANNEL_STATE: + { + if (QString(switch_event_get_header_nil(event, "Answer-State")) == "early") + { + call->setState(FSPHONE_CALL_STATE_RINGING); + emit ringing(uuid); + } + //printEventHeaders(event); + break; + } + + default: + printf("B leg: %s(%s)\n",switch_event_name(event->event_id), switch_event_get_header_nil(event, "Event-Subclass")); + break; + } + } + return status; +} + +void FSHost::generalEventHandler(switch_event_t *event) +{ + QString uuid = switch_event_get_header_nil(event, "Unique-ID"); + + if (_bleg_uuids.contains(uuid)) + { + if (processBlegEvent(event, uuid) == SWITCH_STATUS_SUCCESS) + { + return; + } + } + if (_active_calls.contains(uuid)) + { + if (processAlegEvent(event, uuid) == SWITCH_STATUS_SUCCESS) + { + return; + } + } + + /* This is how we identify new calls, inbound and outbound */ + switch(event->event_id) { + case SWITCH_EVENT_CUSTOM: + { + if (strcmp(event->subclass_name, "portaudio::ringing") == 0 && !_active_calls.contains(uuid)) + { + Call *call = new Call(atoi(switch_event_get_header_nil(event, "call_id")), + switch_event_get_header_nil(event, "Caller-Caller-ID-Name"), + switch_event_get_header_nil(event, "Caller-Caller-ID-Number"), + FSPHONE_CALL_DIRECTION_INBOUND, + uuid); + _active_calls.insert(uuid, call); + call->setState(FSPHONE_CALL_STATE_RINGING); + emit ringing(uuid); + } + else if (strcmp(event->subclass_name, "portaudio::makecall") == 0) + { + Call *call = new Call(atoi(switch_event_get_header_nil(event, "call_id")),NULL, + switch_event_get_header_nil(event, "Caller-Destination-Number"), + FSPHONE_CALL_DIRECTION_OUTBOUND, + uuid); + _active_calls.insert(uuid, call); + call->setState(FSPHONE_CALL_STATE_TRYING); + emit newOutgoingCall(uuid); + } + else if (strcmp(event->subclass_name, "sofia::gateway_state") == 0) + { + QString state = switch_event_get_header_nil(event, "State"); + QString gw = switch_event_get_header_nil(event, "Gateway"); + if (state == "TRYING") + emit gwStateChange(gw, FSPHONE_GW_STATE_TRYING); + else if (state == "REGISTER") + emit gwStateChange(gw, FSPHONE_GW_STATE_REGISTER); + else if (state == "REGED") + emit gwStateChange(gw, FSPHONE_GW_STATE_REGED); + else if (state == "UNREGED") + emit gwStateChange(gw, FSPHONE_GW_STATE_UNREGED); + else if (state == "UNREGISTER") + emit gwStateChange(gw, FSPHONE_GW_STATE_UNREGISTER); + else if (state =="FAILED") + emit gwStateChange(gw, FSPHONE_GW_STATE_FAILED); + else if (state == "FAIL_WAIT") + emit gwStateChange(gw, FSPHONE_GW_STATE_FAIL_WAIT); + else if (state == "EXPIRED") + emit gwStateChange(gw, FSPHONE_GW_STATE_EXPIRED); + else if (state == "NOREG") + emit gwStateChange(gw, FSPHONE_GW_STATE_NOREG); + } + else + { + //printf("We got a not treated custom event: %s\n", (!zstr(event->subclass_name) ? event->subclass_name : "NULL")); + } + break; + } + default: + break; + } +} + +switch_status_t FSHost::sendCmd(const char *cmd, const char *args, QString *res) +{ + switch_status_t status = SWITCH_STATUS_FALSE; + switch_stream_handle_t stream = { 0 }; + SWITCH_STANDARD_STREAM(stream); + status = switch_api_execute(cmd, args, NULL, &stream); + *res = switch_str_nil((char *) stream.data); + + return status; +} + +void FSHost::printEventHeaders(switch_event_t *event) +{ + switch_event_header_t *hp; + printf("Received event: %s(%s)\n", switch_event_name(event->event_id), switch_event_get_header_nil(event, "Event-Subclass")); + for (hp = event->headers; hp; hp = hp->next) { + printf("%s=%s\n", hp->name, hp->value); + } + printf("\n\n"); +} diff --git a/fscomm/fshost.h b/fscomm/fshost.h new file mode 100644 index 0000000000..c3858cd8ed --- /dev/null +++ b/fscomm/fshost.h @@ -0,0 +1,106 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ +#ifndef FSHOST_H +#define FSHOST_H + +#include +#include +#include + +class Call; + +#define FSPHONE_GW_STATE_TRYING 0 +#define FSPHONE_GW_STATE_REGISTER 1 +#define FSPHONE_GW_STATE_REGED 2 +#define FSPHONE_GW_STATE_UNREGED 3 +#define FSPHONE_GW_STATE_UNREGISTER 4 +#define FSPHONE_GW_STATE_FAILED 5 +#define FSPHONE_GW_STATE_FAIL_WAIT 6 +#define FSPHONE_GW_STATE_EXPIRED 7 +#define FSPHONE_GW_STATE_NOREG 8 + +static const char *fsphone_gw_state_names[] = { + "TRYING", + "REGISTER", + "REGED", + "UNREGED", + "UNREGISTER", + "FAILED", + "FAIL_WAIT", + "EXPIRED", + "NOREG" +}; + +class FSHost : public QThread +{ +Q_OBJECT +public: + explicit FSHost(QObject *parent = 0); + switch_status_t sendCmd(const char *cmd, const char *args, QString *res); + void generalEventHandler(switch_event_t *event); + Call * getCallByUUID(QString uuid) { return _active_calls.value(uuid, NULL); } + QString getGwStateName(int id) { return fsphone_gw_state_names[id]; } + +protected: + void run(void); + +signals: + void coreLoadingError(QString); + void ready(void); + void ringing(QString); + void answered(QString); + void newOutgoingCall(QString); + void callFailed(QString); + void hungup(Call*); + void gwStateChange(QString, int); + +private: + switch_status_t processBlegEvent(switch_event_t *, QString); + switch_status_t processAlegEvent(switch_event_t *, QString); + void printEventHeaders(switch_event_t *event); + QHash _active_calls; + QHash _bleg_uuids; +}; + +extern FSHost g_FSHost; + +/* + Used to match callback from fs core. We dup the event and call the class + method callback to make use of the signal/slot infrastructure. +*/ +static void eventHandlerCallback(switch_event_t *event) +{ + switch_event_t *clone = NULL; + if (switch_event_dup(&clone, event) == SWITCH_STATUS_SUCCESS) { + g_FSHost.generalEventHandler(clone); + } + switch_safe_free(clone); +} + +#endif // FSHOST_H diff --git a/fscomm/main.cpp b/fscomm/main.cpp new file mode 100644 index 0000000000..67154bb036 --- /dev/null +++ b/fscomm/main.cpp @@ -0,0 +1,51 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ + +#include +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + QCoreApplication::setOrganizationName("FreeSWITCH"); + QCoreApplication::setOrganizationDomain("freeswitch.org"); + QCoreApplication::setApplicationName("FSPhone"); + + QPixmap image(":/images/splash.png"); + QSplashScreen *splash = new QSplashScreen(image); + splash->show(); + splash->showMessage("Loading, please wait...", Qt::AlignRight|Qt::AlignBottom, Qt::blue); + + QObject::connect(&g_FSHost, SIGNAL(ready()), splash, SLOT(close())); + MainWindow w; + QObject::connect(&g_FSHost, SIGNAL(ready()), &w, SLOT(show())); + g_FSHost.start(); + return a.exec(); +} diff --git a/fscomm/mainwindow.cpp b/fscomm/mainwindow.cpp new file mode 100644 index 0000000000..f460b0db8c --- /dev/null +++ b/fscomm/mainwindow.cpp @@ -0,0 +1,334 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ + +#include +#include +#include "mainwindow.h" +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + preferences(NULL) +{ + ui->setupUi(this); + + dialpadMapper = new QSignalMapper(this); + connect(ui->dtmf0Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf1Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf2Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf3Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf4Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf5Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf6Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf7Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf8Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmf9Btn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmfABtn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmfBBtn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmfCBtn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmfDBtn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmfAstBtn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + connect(ui->dtmfPoundBtn, SIGNAL(clicked()), dialpadMapper, SLOT(map())); + dialpadMapper->setMapping(ui->dtmf0Btn, QString("0")); + dialpadMapper->setMapping(ui->dtmf1Btn, QString("1")); + dialpadMapper->setMapping(ui->dtmf2Btn, QString("2")); + dialpadMapper->setMapping(ui->dtmf3Btn, QString("3")); + dialpadMapper->setMapping(ui->dtmf4Btn, QString("4")); + dialpadMapper->setMapping(ui->dtmf5Btn, QString("5")); + dialpadMapper->setMapping(ui->dtmf6Btn, QString("6")); + dialpadMapper->setMapping(ui->dtmf7Btn, QString("7")); + dialpadMapper->setMapping(ui->dtmf8Btn, QString("8")); + dialpadMapper->setMapping(ui->dtmf9Btn, QString("9")); + dialpadMapper->setMapping(ui->dtmfABtn, QString("A")); + dialpadMapper->setMapping(ui->dtmfBBtn, QString("B")); + dialpadMapper->setMapping(ui->dtmfCBtn, QString("C")); + dialpadMapper->setMapping(ui->dtmfDBtn, QString("D")); + dialpadMapper->setMapping(ui->dtmfAstBtn, QString("*")); + dialpadMapper->setMapping(ui->dtmfPoundBtn, QString("#")); + connect(dialpadMapper, SIGNAL(mapped(QString)), this, SLOT(dialDTMF(QString))); + + connect(&g_FSHost, SIGNAL(ready()),this, SLOT(fshostReady())); + connect(&g_FSHost, SIGNAL(ringing(QString)), this, SLOT(ringing(QString))); + connect(&g_FSHost, SIGNAL(answered(QString)), this, SLOT(answered(QString))); + connect(&g_FSHost, SIGNAL(hungup(Call*)), this, SLOT(hungup(Call*))); + connect(&g_FSHost, SIGNAL(newOutgoingCall(QString)), this, SLOT(newOutgoingCall(QString))); + connect(&g_FSHost, SIGNAL(gwStateChange(QString,int)), this, SLOT(gwStateChanged(QString,int))); + /*connect(&g_FSHost, SIGNAL(coreLoadingError(QString)), this, SLOT(coreLoadingError(QString)));*/ + + connect(ui->newCallBtn, SIGNAL(clicked()), this, SLOT(makeCall())); + connect(ui->answerBtn, SIGNAL(clicked()), this, SLOT(paAnswer())); + connect(ui->hangupBtn, SIGNAL(clicked()), this, SLOT(paHangup())); + connect(ui->listCalls, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(callListDoubleClick(QListWidgetItem*))); + connect(ui->action_Preferences, SIGNAL(triggered()), this, SLOT(prefTriggered())); + connect(ui->action_Exit, SIGNAL(triggered()), this, SLOT(close())); +} + +MainWindow::~MainWindow() +{ + delete ui; + QString res; + g_FSHost.sendCmd("fsctl", "shutdown", &res); + g_FSHost.wait(); +} + +void MainWindow::prefTriggered() +{ + if (!preferences) + preferences = new PrefDialog(); + + preferences->raise(); + preferences->show(); + preferences->activateWindow(); +} + +void MainWindow::coreLoadingError(QString err) +{ + QMessageBox::warning(this, "Error Loading Core...", err, QMessageBox::Ok); + QApplication::exit(255); +} + +void MainWindow::gwStateChanged(QString gw, int state) +{ + ui->statusBar->showMessage(tr("Account %1 is %2").arg(gw, g_FSHost.getGwStateName(state))); + + /* TODO: This should be placed somewhere else when the config handler is here... */ + QList match = ui->tableAccounts->findItems(gw, Qt::MatchExactly); + if (match.isEmpty()) + { + /* Create the damn thing */ + ui->tableAccounts->setRowCount(ui->tableAccounts->rowCount()+1); + QTableWidgetItem *gwField = new QTableWidgetItem(gw); + QTableWidgetItem *stField = new QTableWidgetItem(g_FSHost.getGwStateName(state)); + ui->tableAccounts->setItem(0,0,gwField); + ui->tableAccounts->setItem(0,1,stField); + ui->tableAccounts->resizeColumnsToContents(); + return; + } + + QTableWidgetItem *gwField = match.at(0); + QTableWidgetItem *stField = ui->tableAccounts->item(gwField->row(),1); + stField->setText(g_FSHost.getGwStateName(state)); + ui->tableAccounts->resizeColumnsToContents(); + +} + +void MainWindow::dialDTMF(QString dtmf) +{ + QString result; + QString dtmf_string = QString("dtmf %1").arg(dtmf); + if (g_FSHost.sendCmd("pa", dtmf_string.toAscii(), &result) == SWITCH_STATUS_FALSE) { + ui->textEdit->setText("Error sending that command"); + } +} + +void MainWindow::callListDoubleClick(QListWidgetItem *item) +{ + Call *call = g_FSHost.getCallByUUID(item->data(Qt::UserRole).toString()); + QString switch_str = QString("switch %1").arg(call->getCallID()); + QString result; + if (g_FSHost.sendCmd("pa", switch_str.toAscii(), &result) == SWITCH_STATUS_FALSE) { + ui->textEdit->setText(QString("Error switching to call %1").arg(call->getCallID())); + return; + } + ui->hangupBtn->setEnabled(true); +} + +void MainWindow::makeCall() +{ + bool ok; + QString dialstring = QInputDialog::getText(this, tr("Make new call"), + tr("Number to dial:"), QLineEdit::Normal, NULL,&ok); + + if (ok && !dialstring.isEmpty()) + { + paCall(dialstring); + } +} + +void MainWindow::fshostReady() +{ + ui->statusBar->showMessage("Ready"); + ui->newCallBtn->setEnabled(true); + ui->textEdit->setEnabled(true); + ui->textEdit->setText("Ready to dial and receive calls!"); +} + +void MainWindow::paAnswer() +{ + QString result; + if (g_FSHost.sendCmd("pa", "answer", &result) == SWITCH_STATUS_FALSE) { + ui->textEdit->setText("Error sending that command"); + } + + ui->textEdit->setText("Talking..."); + ui->hangupBtn->setEnabled(true); + ui->answerBtn->setEnabled(false); +} + +void MainWindow::paCall(QString dialstring) +{ + QString result; + + QString callstring = QString("call %1").arg(dialstring); + + if (g_FSHost.sendCmd("pa", callstring.toAscii(), &result) == SWITCH_STATUS_FALSE) { + ui->textEdit->setText("Error sending that command"); + } + + ui->hangupBtn->setEnabled(true); +} + +void MainWindow::paHangup() +{ + QString result; + if (g_FSHost.sendCmd("pa", "hangup", &result) == SWITCH_STATUS_FALSE) { + ui->textEdit->setText("Error sending that command"); + } + + ui->textEdit->setText("Click to dial number..."); + ui->statusBar->showMessage("Call hungup"); + ui->hangupBtn->setEnabled(false); +} + +void MainWindow::newOutgoingCall(QString uuid) +{ + Call *call = g_FSHost.getCallByUUID(uuid); + ui->textEdit->setText(QString("Calling %1 (%2)").arg(call->getCidName(), call->getCidNumber())); + QListWidgetItem *item = new QListWidgetItem(tr("%1 (%2) - Calling").arg(call->getCidName(), call->getCidNumber())); + item->setData(Qt::UserRole, uuid); + ui->listCalls->addItem(item); + ui->hangupBtn->setEnabled(true); +} + +void MainWindow::ringing(QString uuid) +{ + + Call *call = g_FSHost.getCallByUUID(uuid); + for (int i=0; ilistCalls->count(); i++) + { + QListWidgetItem *item = ui->listCalls->item(i); + if (item->data(Qt::UserRole).toString() == uuid) + { + item->setText(tr("%1 - Ringing").arg(call->getCidNumber())); + ui->textEdit->setText(QString("Call from %1 (%2)").arg(call->getCidName(), call->getCidNumber())); + return; + } + } + + ui->textEdit->setText(QString("Call from %1 (%2)").arg(call->getCidName(), call->getCidNumber())); + QListWidgetItem *item = new QListWidgetItem(tr("%1 (%2) - Ringing").arg(call->getCidName(), call->getCidNumber())); + item->setData(Qt::UserRole, uuid); + ui->listCalls->addItem(item); + ui->answerBtn->setEnabled(true); +} + +void MainWindow::answered(QString uuid) +{ + Call *call = g_FSHost.getCallByUUID(uuid); + for (int i=0; ilistCalls->count(); i++) + { + QListWidgetItem *item = ui->listCalls->item(i); + if (item->data(Qt::UserRole).toString() == uuid) + { + if (call->getDirection() == FSPHONE_CALL_DIRECTION_INBOUND) + { + item->setText(tr("%1 (%2) - Active").arg(call->getCidName(), call->getCidNumber())); + break; + } + else + { + item->setText(tr("%1 - Active").arg(call->getCidNumber())); + break; + } + } + } + ui->dtmf0Btn->setEnabled(true); + ui->dtmf1Btn->setEnabled(true); + ui->dtmf2Btn->setEnabled(true); + ui->dtmf3Btn->setEnabled(true); + ui->dtmf4Btn->setEnabled(true); + ui->dtmf5Btn->setEnabled(true); + ui->dtmf6Btn->setEnabled(true); + ui->dtmf7Btn->setEnabled(true); + ui->dtmf8Btn->setEnabled(true); + ui->dtmf9Btn->setEnabled(true); + ui->dtmfABtn->setEnabled(true); + ui->dtmfBBtn->setEnabled(true); + ui->dtmfCBtn->setEnabled(true); + ui->dtmfDBtn->setEnabled(true); + ui->dtmfAstBtn->setEnabled(true); + ui->dtmfPoundBtn->setEnabled(true); +} + +void MainWindow::hungup(Call* call) +{ + for (int i=0; ilistCalls->count(); i++) + { + QListWidgetItem *item = ui->listCalls->item(i); + if (item->data(Qt::UserRole).toString() == call->getUUID()) + { + delete ui->listCalls->takeItem(i); + break; + } + } + ui->textEdit->setText(tr("Call with %1 (%2) hungup.").arg(call->getCidName(), call->getCidNumber())); + /* TODO: Will cause problems if 2 calls are received at the same time */ + ui->answerBtn->setEnabled(false); + ui->hangupBtn->setEnabled(false); + ui->dtmf0Btn->setEnabled(false); + ui->dtmf1Btn->setEnabled(false); + ui->dtmf2Btn->setEnabled(false); + ui->dtmf3Btn->setEnabled(false); + ui->dtmf4Btn->setEnabled(false); + ui->dtmf5Btn->setEnabled(false); + ui->dtmf6Btn->setEnabled(false); + ui->dtmf7Btn->setEnabled(false); + ui->dtmf8Btn->setEnabled(false); + ui->dtmf9Btn->setEnabled(false); + ui->dtmfABtn->setEnabled(false); + ui->dtmfBBtn->setEnabled(false); + ui->dtmfCBtn->setEnabled(false); + ui->dtmfDBtn->setEnabled(false); + ui->dtmfAstBtn->setEnabled(false); + ui->dtmfPoundBtn->setEnabled(false); + delete call; +} + +void MainWindow::changeEvent(QEvent *e) +{ + QMainWindow::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} diff --git a/fscomm/mainwindow.h b/fscomm/mainwindow.h new file mode 100644 index 0000000000..a6166642d8 --- /dev/null +++ b/fscomm/mainwindow.h @@ -0,0 +1,80 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ + + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include + +namespace Ui { + class MainWindow; +} + +class MainWindow : public QMainWindow { + Q_OBJECT +public: + MainWindow(QWidget *parent = 0); + ~MainWindow(); + +protected: + void changeEvent(QEvent *e); + +signals: + void dtmfDialed(QString); + +private slots: + void prefTriggered(); + void coreLoadingError(QString); + void gwStateChanged(QString, int); + void dialDTMF(QString); + void callListDoubleClick(QListWidgetItem *); + void makeCall(); + void fshostReady(); + void paAnswer(); + void paCall(QString); + void paHangup(); + void newOutgoingCall(QString); + void ringing(QString); + void answered(QString); + void hungup(Call*); + +private: + Ui::MainWindow *ui; + QSignalMapper *dialpadMapper; + PrefDialog *preferences; +}; + +#endif // MAINWINDOW_H diff --git a/fscomm/mainwindow.ui b/fscomm/mainwindow.ui new file mode 100644 index 0000000000..700c09358c --- /dev/null +++ b/fscomm/mainwindow.ui @@ -0,0 +1,350 @@ + + + MainWindow + + + + 0 + 0 + 580 + 563 + + + + FSPhone - A FreeSWITCH softphone + + + + + + + + + false + + + true + + + + + + + + + false + + + New Call + + + + + + + + + + + false + + + Answer + + + + + + + false + + + Hangup + + + + + + + + + + + false + + + 1 + + + + + + + false + + + 2 + + + + + + + false + + + 3 + + + + + + + false + + + 4 + + + + + + + false + + + 5 + + + + + + + false + + + 6 + + + + + + + false + + + 7 + + + + + + + false + + + 8 + + + + + + + false + + + 9 + + + + + + + false + + + * + + + + + + + false + + + 0 + + + + + + + false + + + # + + + + + + + false + + + A + + + + + + + false + + + B + + + + + + + false + + + D + + + + + + + false + + + C + + + + + + + + + + + + + Active Calls + + + + + + + + + + + + Active Accounts + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + 0 + + + true + + + false + + + false + + + false + + + + Account + + + + + Status + + + + + + + + + + + + + + + 0 + 0 + 580 + 22 + + + + + &File + + + + + + + + + + TopToolBarArea + + + false + + + + + + &Preferences + + + + + &Exit + + + + + + + diff --git a/fscomm/mod_qsettings/mod_qsettings.cpp b/fscomm/mod_qsettings/mod_qsettings.cpp new file mode 100644 index 0000000000..b09b305fc4 --- /dev/null +++ b/fscomm/mod_qsettings/mod_qsettings.cpp @@ -0,0 +1,149 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + * + * Description: + * Module to load configurations from Qt preference system QSettings + * + */ +#include +#include +#include +#include "mod_qsettings/mod_qsettings.h" + +static struct { + switch_memory_pool_t* pool; +} globals; + +switch_xml_t XMLBinding::getConfigXML(QString tmpl) +{ + switch_event_t *e; + switch_event_create_plain(&e, SWITCH_EVENT_REQUEST_PARAMS); + switch_assert(e); + + QFile tmplFile(QString("%1/templates/%2.xml").arg(QApplication::applicationDirPath(),tmpl)); + + if (!tmplFile.exists()) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, + "Template %s.xml, doesn't exist on directory, falling back to embedded template.\n", + tmpl.toAscii().constData()); + tmplFile.setFileName(QString(":/confs/%1.xml").arg(tmpl)); + return NULL; + } + + if (tmplFile.open(QIODevice::ReadOnly | QIODevice::Text)) + { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Template %s could not be read.!\n", tmpl.toAscii().constData()); + return NULL; + } + + /* Open template file and expand all strings based on QSettings */ + QString tmplContents(tmplFile.readAll()); + tmplFile.close(); + _settings->beginGroup("FreeSIWTCH/conf"); + _settings->beginGroup(tmpl); + foreach(QString k, _settings->childKeys()) + { + switch_event_add_header_string(e, SWITCH_STACK_BOTTOM, k.toAscii().constData(), _settings->value(k).toByteArray().constData()); + } + + char *res = switch_event_expand_headers(e, tmplContents.toAscii().constData()); + switch_safe_free(e); + return switch_xml_parse_str(res, strlen(res)); +} + +static switch_xml_t xml_url_fetch(const char *section, const char *tag_name, const char *key_name, const char *key_value, switch_event_t *params, + void *user_data) +{ + XMLBinding *binding = (XMLBinding *) user_data; + + if (!binding) { + return NULL; + } + + return binding->getConfigXML(key_value); +} + +static switch_status_t do_config(void) +{ + char *cf = "qsettings.conf"; + switch_xml_t cfg, xml, bindings_tag; + XMLBinding *binding = NULL; + + if (!(xml = switch_xml_open_cfg(cf, &cfg, NULL))) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "open of %s failed\n", cf); + return SWITCH_STATUS_TERM; + } + + if (!(bindings_tag = switch_xml_child(cfg, "bindings"))) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Missing tag!\n"); + switch_xml_free(xml); + return SWITCH_STATUS_FALSE; + } + + QString bind_mask = switch_xml_attr_soft(bindings_tag, "value"); + if (!bind_mask.isEmpty()) + { + binding = new XMLBinding(bind_mask); + } + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Binding XML Fetch Function [%s]\n", + binding->getBinding().isEmpty() ? "all" : binding->getBinding().toAscii().constData()); + switch_xml_bind_search_function(xml_url_fetch, switch_xml_parse_section_string(binding->getBinding().toAscii().constData()), binding); + binding = NULL; + + switch_xml_free(xml); + return SWITCH_STATUS_SUCCESS; +} + +SWITCH_MODULE_LOAD_FUNCTION(mod_qsettings_load) +{ + /*switch_api_interface_t *qsettings_api_interface;*/ + + /* connect my internal structure to the blank pointer passed to me */ + *module_interface = switch_loadable_module_create_module_interface(pool, "mod_qsettings"); + + memset(&globals,0,sizeof(globals)); + globals.pool = pool; + + if (do_config() == SWITCH_STATUS_SUCCESS) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Sucessfully configured.\n"); + } else { + return SWITCH_STATUS_FALSE; + } + + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "We loaded mod_qsettings.\n"); + /* indicate that the module should continue to be loaded */ + return SWITCH_STATUS_SUCCESS; +} + +SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_qsettings_shutdown) +{ + switch_xml_unbind_search_function_ptr(xml_url_fetch); + return SWITCH_STATUS_SUCCESS; +} diff --git a/fscomm/mod_qsettings/mod_qsettings.h b/fscomm/mod_qsettings/mod_qsettings.h new file mode 100644 index 0000000000..776e3cba02 --- /dev/null +++ b/fscomm/mod_qsettings/mod_qsettings.h @@ -0,0 +1,51 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2009, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Joao Mesquita + * + */ + +#ifndef MOD_QSETTINGS_H +#define MOD_QSETTINGS_H + +#include +#include +#include + +SWITCH_MODULE_LOAD_FUNCTION(mod_qsettings_load); +SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_qsettings_shutdown); + +class XMLBinding +{ +public: + XMLBinding(QString binding) : _binding(binding), _settings(new QSettings) {} + QString getBinding(void) { return _binding; } + switch_xml_t getConfigXML(QString); +private: + QString _binding; + QSettings* _settings; +}; + +#endif // MOD_QSETTINGS_H diff --git a/fscomm/prefdialog.cpp b/fscomm/prefdialog.cpp new file mode 100644 index 0000000000..8f842a5c41 --- /dev/null +++ b/fscomm/prefdialog.cpp @@ -0,0 +1,132 @@ +#include +#include "prefdialog.h" +#include "ui_prefdialog.h" + +PrefDialog::PrefDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::PrefDialog) +{ + ui->setupUi(this); + getPaDevlist(); +} + +PrefDialog::~PrefDialog() +{ + delete ui; +} + +void PrefDialog::getPaDevlist() +{ + QString result; + int errorLine, errorColumn; + QString errorMsg; + + if (g_FSHost.sendCmd("pa", "devlist xml", &result) != SWITCH_STATUS_SUCCESS) + { + QMessageBox::critical(this, tr("PortAudio error" ), + tr("Error querying audio devices."), + QMessageBox::Ok); + return; + } + + if (!_xmlPaDevList.setContent(result, &errorMsg, &errorLine, &errorColumn)) + { + QMessageBox::critical(this, tr("PortAudio error" ), + tr("Error parsing output xml from pa devlist.\n%1 (Line:%2, Col:%3).").arg(errorMsg, + errorLine, + errorColumn), + QMessageBox::Ok); + return; + } + QDomElement root = _xmlPaDevList.documentElement(); + if (root.tagName() != "xml") + { + QMessageBox::critical(this, tr("PortAudio error" ), + tr("Error parsing output xml from pa devlist. Root tag is not ."), + QMessageBox::Ok); + return; + } + QDomElement devices = root.firstChildElement("devices"); + if (devices.isNull()) + { + QMessageBox::critical(this, tr("PortAudio error" ), + tr("Error parsing output xml from pa devlist. There is no tag."), + QMessageBox::Ok); + return; + } + + QDomElement child = devices.firstChildElement(); + if (child.isNull()) + { + QMessageBox::critical(this, tr("PortAudio error" ), + tr("Error parsing output xml from pa devlist. There is no tag."), + QMessageBox::Ok); + return; + } + + while (!child.isNull()) + { + if (child.tagName() == "device") + { + QString id, name, inputs, outputs; + id = child.attribute("id","-1"); + name = child.attribute("name","Null"); + inputs = child.attribute("inputs","0"); + outputs = child.attribute("outputs","0"); + if (inputs.toInt() != 0) + ui->PaIndevCombo->addItem(name,inputs.toInt()); + if (outputs.toInt() != 0) + { + ui->PaOutdevCombo->addItem(name,inputs.toInt()); + ui->PaRingdevCombo->addItem(name,inputs.toInt()); + } + } + child = child.nextSiblingElement(); + } + + QDomElement bindings = root.firstChildElement("bindings"); + if (bindings.isNull()) + { + QMessageBox::critical(this, tr("PortAudio error" ), + tr("Error parsing output xml from pa devlist. There is no tag."), + QMessageBox::Ok); + return; + } + + child = devices.firstChildElement(); + if (child.isNull()) + { + QMessageBox::critical(this, tr("PortAudio error" ), + tr("Error parsing output xml from pa devlist. There are no bindings."), + QMessageBox::Ok); + return; + } + + while (!child.isNull()) + { + QString id; + id = child.attribute("device","-1"); + + if (child.tagName() == "ring") + ui->PaRingdevCombo->setCurrentIndex(id.toInt()); + else if (child.tagName() == "input") + ui->PaIndevCombo->setCurrentIndex(id.toInt()); + else if (child.tagName() == "ring") + ui->PaOutdevCombo->setCurrentIndex(id.toInt()); + + child = child.nextSiblingElement(); + } + +} + +void PrefDialog::changeEvent(QEvent *e) +{ + QDialog::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} diff --git a/fscomm/prefdialog.h b/fscomm/prefdialog.h new file mode 100644 index 0000000000..4a73498f11 --- /dev/null +++ b/fscomm/prefdialog.h @@ -0,0 +1,27 @@ +#ifndef PREFDIALOG_H +#define PREFDIALOG_H + +#include +#include +#include + +namespace Ui { + class PrefDialog; +} + +class PrefDialog : public QDialog { + Q_OBJECT +public: + PrefDialog(QWidget *parent = 0); + ~PrefDialog(); + +protected: + void changeEvent(QEvent *e); + +private: + void getPaDevlist(void); + Ui::PrefDialog *ui; + QDomDocument _xmlPaDevList; +}; + +#endif // PREFDIALOG_H diff --git a/fscomm/prefdialog.ui b/fscomm/prefdialog.ui new file mode 100644 index 0000000000..68c1683647 --- /dev/null +++ b/fscomm/prefdialog.ui @@ -0,0 +1,376 @@ + + + PrefDialog + + + + 0 + 0 + 477 + 356 + + + + Preferences + + + + + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + false + + + QAbstractItemView::NoDragDrop + + + + 96 + 84 + + + + QListView::Static + + + QListView::LeftToRight + + + true + + + 12 + + + QListView::IconMode + + + + Sofia + + + + :/images/pref_sip.png:/images/pref_sip.png + + + + + PortAudio + + + + :/images/pref_audio.gif:/images/pref_audio.gif + + + + + + + + 1 + + + + + + + Global Settings + + + + + + log-level + + + + + + + + + + auto-restart + + + + + + + + + + debug-presence + + + + + + + + + + + + + Softphone Profile + + + + + + user-agent-string + + + + + + + + + + debug + + + + + + + + + + sip-trace + + + + + + + + + + + + + + + + + Devices + + + + + 72 + 34 + 34 + 17 + + + + indev + + + + + + 111 + 32 + 111 + 26 + + + + + + + 63 + 64 + 43 + 17 + + + + outdev + + + + + + 111 + 62 + 111 + 26 + + + + + + + 59 + 94 + 47 + 17 + + + + rindgev + + + + + + 111 + 92 + 111 + 26 + + + + + + + + + Files + + + + + + ring-file + + + + + + + tone_stream://%(2000,4000,440.0,480.0);loops=20 + + + + + + + ring-interval + + + + + + + + + + hold-file + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + buttonBox + accepted() + PrefDialog + accept() + + + 544 + 224 + + + 157 + 246 + + + + + buttonBox + rejected() + PrefDialog + reject() + + + 544 + 230 + + + 286 + 246 + + + + + listSections + currentRowChanged(int) + stackedWidget + setCurrentIndex(int) + + + 180 + 83 + + + 524 + 102 + + + + + diff --git a/fscomm/resources.qrc b/fscomm/resources.qrc new file mode 100644 index 0000000000..992ac3b6ff --- /dev/null +++ b/fscomm/resources.qrc @@ -0,0 +1,13 @@ + + + resources/splash.png + resources/pref_sip.png + resources/pref_audio.gif + + + conf/freeswitch.xml + conf/accounts/example.xml + conf/event_socket.conf.xml + conf/portaudio.conf.xml + + diff --git a/fscomm/resources/pref_audio.gif b/fscomm/resources/pref_audio.gif new file mode 100644 index 0000000000000000000000000000000000000000..862963bbe850bd0c2c669def1439e1cfb4c3db12 GIT binary patch literal 32099 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 literal 0 HcmV?d00001 diff --git a/fscomm/resources/pref_sip.png b/fscomm/resources/pref_sip.png new file mode 100644 index 0000000000000000000000000000000000000000..7bac8568585a7003cf11384b8b9f4b3ffd1df0db GIT binary patch literal 1987 zcmV;!2R!(RP)TZwIt3WUJeMv4{a zD@nDRSa}a11jc4ktU~|9L_3L91c4A33(4c31WiXK1GQQWDwPTp3I)2XAH;DSIGs-3 zB8=V3Y4;TX(nu<=2KF!v13H}!L5K*(VzKaLh|UrlC+q0wm_Z)=8M;TW*CPxGpqNai z@Rl-<@rSiR(QXQ1h%dQZ&igt{r4&e6=oUI=prla*UzbrJW5UUyq-!N%DxyG?;n5XJ z8b$E6nnAb3mk7`k z#na}4*JT5r%bKttyjB)0Hw=m2LZ~?M&-ff>zD!hcoj2fgn;QxT0uZ159&qM6#99*; zh|$79yVZ+>I!0M21cE{!2n2;d5C{r^3^>Kzs%qu%5>S`C8LZn+)dJULL?Dtk)zoy* z&)W>@;u(VNX>Y6n%dsE8x_?8&=TUf>i>vv^w)!1lu3IPAKlro51afi2=RUaF$d~Tc zW^ft~g3DjWE{ljdH5^7jy2;BS<(+S%wy1!dnWMqTTEq4iM}5zf!il_T%PJtNavx~l z+r)n{k{*cWrFpz#9;NKk4}hDry4TmTKk)SVbELmAADHPIfb;s28;G`SnV@bwQUhnU zltX9pF~RoyIcvc*sPFk!OUpU_HABs9Hw%s#Qn~=lgU2WL7!um8@Gv$nfvhw&+&1im zd!-dHa^W|@{Z@4jq;y^ajq3k@bi=Ms!G5^`~4*(SAw}_38bYe zG3;$?s^ejd7?K9rq|}_-1J=t8{65-h+M9ld2Oh4^ub-bi2#SX51;!ls9J>BF5IiR5 z<=Jq@?7*;>%40J@hBig8&AO)?E_(FP_U|buAfWWx{=e*wx$+ml__u;}8>HgIkSeQW zGRPU>$MQPNkmG8BF?kt$s}Vz7*G=I7KVoc52KTrCLgxViJu~YQu*-UWJLQb5Pvgep zN}l90tRn?e2luOEZrqu)TGc{o!}d-{1uU3&`H@8*EFt+`*` zBY)~5&^V?;ir>-7+!DTJ$TRW;$6eiD1)7xDxF&9ZxLXa-wq-U9TU8f?Ld)#Ref)NL z$HgZHGG)7y*_bn1Lh$n2Ryp^%)6R`mC$Gxy``rtqyptY|Lb!ro|5 zvRCXAB@E?T#E}{}x$#{dhT3a`qFjA)r%s|l3GKe!$*1IND=7 zZ4s#?q#l{06J^qw@IV4kI(}IR!z%W4j{!4RgW>4+FjU?5`2kIp#3DzPuw7QDe3uSE z#~&L7a}?&;=`l@@Cp8d0rE%g%K|Qm0GTgJd5J;><^H`y&70r+WxbJWy5V1th7Uq0) zg+HJEs5wfgo+SN_Kzd18=;u}eqt=0D!dz(eW=1?tRX7QfMKJ~9Y!=*m6GpGD1y|U& zUsX6!klKh8wjz-J8nC;JU_DtItkb#1par`@o|zwnk+Gyk@SOIhI!IG12P}}7&qL^X ztJIR2AlE$1><*qV3xHY#wV$j5^Yycko0owtM9e6vv%2@BpQGajzxaX9q{Tc@g<&s| z=UO8eH@)3sNlrjC-$e6FGS_{6@k2P!U^Hek^#ty;JaRvFSsdPI5#hdyUv)lfZiO8VJ`}SAP^J+K_L(X zfLqHxf&CY+kUteFFoC(RdTiw0nTHB`y%DmQ~PSX`>mp;H9To zSy}iiI}*zOyTdq!;Gm8|AP5A7KoAHDfglhR0zn}V1cE{!2n6-yKm?Z~`FuX~ibRUw zYbOPAA#@AJap)Z&XjOL&)cn%IOul(jK*kJsx3v)K$bn+?a5 zeg{ VsAm2Fel`FA002ovPDHLkV1i*@thWFF literal 0 HcmV?d00001 diff --git a/fscomm/resources/splash.png b/fscomm/resources/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..47e7acb06dde54644201f586dad45748e3ebfcc0 GIT binary patch literal 6418 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 literal 0 HcmV?d00001