Create a plugin system

- Add feedback on errors
- More generic import requests
- Replace storing password by a native popup that ask passwork when needed
- Store passwords in settings file
- Import Feedback in advanced settings tab
- Use English as default translator if no translations are found
- Add OpenSSL packaging for Windows to deal with Qt connections
- Add option to overwrite plugin if exists
- Plugin load/unload managment. Hot-Dynamic load of plugins. Safely test the loaded plugin
- Set plugin data with default value when all GUI items are loaded
- Rewrite folder priority
- Add filename info from pluginloader
- Add plugin versionning
- Specify inputs for saving
- Copy desktop headers in OUTPUT to be used by external projects
- Add a plugins folder auto-managed by cmake
- Remove obsolete contact api submodule
- Clean plugin example
- Add specific behaviour for plugin type : inputs have been splitted by Capability.
- Update save/load to be more generic and add clean function for configurations
- Instantiate Importer List model
- Add IDE integration for plugins
- Set input fields to be dependent of capability
- Change signals interface to take account capability
This commit is contained in:
Julien Wadel 2021-01-13 21:04:23 +01:00
parent de06195c32
commit 5eba9a5ece
60 changed files with 2552 additions and 135 deletions

10
.gitmodules vendored
View file

@ -1,6 +1,6 @@
[submodule "submodules/externals/minizip"]
path = submodules/externals/minizip
url = ../../public/external/minizip.git
[submodule "linphone-sdk"]
path = linphone-sdk
url = ../../public/linphone-sdk.git
path = linphone-sdk
url = https://gitlab.linphone.org/BC/public/linphone-sdk.git
[submodule "plugins/contacts/contacts-api"]
path = plugins/contacts/contacts-api
url = https://gitlab.linphone.org/BC/public/linphone-desktop-plugins/contacts/contacts-api.git

View file

@ -58,7 +58,7 @@ set(LINPHONE_OUTPUT_DIR "${CMAKE_BINARY_DIR}/linphone-sdk/desktop")
set(APPLICATION_OUTPUT_DIR "${CMAKE_BINARY_DIR}/OUTPUT")
set(CMAKE_PREFIX_PATH "${LINPHONE_OUTPUT_DIR};${APPLICATION_OUTPUT_DIR}${PREFIX_PATH}")
set(CMAKE_PREFIX_PATH "${LINPHONE_OUTPUT_DIR};${APPLICATION_OUTPUT_DIR};${APPLICATION_OUTPUT_DIR}/include${PREFIX_PATH}")
string(REPLACE ";" "|" PREFIX_PATH "${CMAKE_PREFIX_PATH}")
#set(PREFIX_PATH "${LINPHONE_OUTPUT_DIR}|${APPLICATION_OUTPUT_DIR}${PREFIX_PATH}")
@ -95,6 +95,10 @@ option(ENABLE_FFMPEG "Build mediastreamer2 with ffmpeg video support." YES)
option(ENABLE_BUILD_VERBOSE "Enable the build generation to be more verbose" NO)
option(ENABLE_OPENH264 "Enable the use of OpenH264 codec" YES)
option(ENABLE_NON_FREE_CODECS "Enable the use of non free codecs" YES)
option(ENABLE_BUILD_APP_PLUGINS "Enable the build of plugins" YES)
option(ENABLE_BUILD_EXAMPLES "Enable the build of examples" NO)
if(WIN32 OR APPLE)
else()
@ -114,6 +118,7 @@ list(APPEND APP_OPTIONS "-DENABLE_FFMPEG=${ENABLE_FFMPEG}")
list(APPEND APP_OPTIONS "-DENABLE_BUILD_VERBOSE=${ENABLE_BUILD_VERBOSE}")
list(APPEND APP_OPTIONS "-DENABLE_OPENH264=${ENABLE_OPENH264}")
list(APPEND APP_OPTIONS "-DENABLE_NON_FREE_CODECS=${ENABLE_NON_FREE_CODECS}")
list(APPEND APP_OPTIONS "-DENABLE_BUILD_EXAMPLES=${ENABLE_BUILD_EXAMPLES}")
if(ENABLE_V4L)
list(APPEND APP_OPTIONS "-DENABLE_V4L=${ENABLE_V4L}")
@ -176,7 +181,6 @@ find_package(belcard CONFIG QUIET)
find_package(Mediastreamer2 CONFIG QUIET)
find_package(ortp CONFIG QUIET)
if(NOT (LinphoneCxx_FOUND) OR NOT (Linphone_FOUND) OR NOT (bctoolbox_FOUND) OR NOT (belcard_FOUND) OR NOT (Mediastreamer2_FOUND) OR NOT (ortp_FOUND) OR FORCE_APP_EXTERNAL_PROJECTS)
message("Projects are set as External projects. You can start building them by using for example : cmake --build . --target install")
ExternalProject_Add(linphone-qt PREFIX "${CMAKE_BINARY_DIR}/linphone-app"
@ -191,16 +195,38 @@ if(NOT (LinphoneCxx_FOUND) OR NOT (Linphone_FOUND) OR NOT (bctoolbox_FOUND) OR N
# ${APP_OPTIONS}
BUILD_ALWAYS ON
)
if( ENABLE_BUILD_APP_PLUGINS)
ExternalProject_Add(app-plugins PREFIX "${CMAKE_BINARY_DIR}/plugins-app"
SOURCE_DIR "${CMAKE_SOURCE_DIR}/plugins"
INSTALL_DIR "${APPLICATION_OUTPUT_DIR}"
BINARY_DIR "${CMAKE_BINARY_DIR}/plugins-app"
DEPENDS ${APP_DEPENDS} linphone-qt
BUILD_COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR> --config $<CONFIG> ${PROJECT_BUILD_COMMAND}
INSTALL_COMMAND ${CMAKE_COMMAND} -E echo "Install step is already done at build time."
LIST_SEPARATOR | # Use the alternate list separator
CMAKE_ARGS ${APP_OPTIONS} ${USER_ARGS} -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_PREFIX_PATH=${PREFIX_PATH}
)
endif()
install(CODE "message(STATUS Running install)")
set(AUTO_REGENERATION auto_regeneration)
add_custom_target(${AUTO_REGENERATION} ALL
COMMAND ${CMAKE_COMMAND} ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS linphone-qt)
if( ENABLE_BUILD_APP_PLUGINS)
add_custom_target(${AUTO_REGENERATION} ALL
COMMAND ${CMAKE_COMMAND} ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS app-plugins)
else()
add_custom_target(${AUTO_REGENERATION} ALL
COMMAND ${CMAKE_COMMAND} ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS linphone-qt)
endif()
else()
message("Adding Linphone Desktop in an IDE-friendly state")
set(CMAKE_INSTALL_PREFIX "${APPLICATION_OUTPUT_DIR}")
add_subdirectory(${CMAKE_SOURCE_DIR}/linphone-app)
add_dependencies(app-library ${APP_DEPENDS})
if( ENABLE_BUILD_APP_PLUGINS)
add_custom_command(TARGET sdk PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/linphone-app/include/" "${CMAKE_INSTALL_PREFIX}/include/")
add_subdirectory(${CMAKE_SOURCE_DIR}/plugins "plugins-app")
endif()
endif()
ExternalProject_Add(linphone-qt-only PREFIX "${CMAKE_BINARY_DIR}/linphone-app"
SOURCE_DIR "${CMAKE_SOURCE_DIR}/linphone-app"

View file

@ -132,6 +132,10 @@ set(SOURCES
src/components/conference/ConferenceModel.cpp
src/components/contact/ContactModel.cpp
src/components/contact/VcardModel.cpp
src/components/contacts/ContactsImporterModel.cpp
src/components/contacts/ContactsImporterPluginsManager.cpp
src/components/contacts/ContactsImporterListModel.cpp
src/components/contacts/ContactsImporterListProxyModel.cpp
src/components/contacts/ContactsListModel.cpp
src/components/contacts/ContactsListProxyModel.cpp
src/components/core/CoreHandlers.cpp
@ -161,6 +165,11 @@ set(SOURCES
src/utils/MediastreamerUtils.cpp
src/utils/QExifImageHeader.cpp
src/utils/Utils.cpp
src/utils/plugins/PluginDataAPI.cpp
src/utils/plugins/PluginNetworkHelper.cpp
src/utils/plugins/LinphonePlugin.cpp
src/utils/plugins/PluginsManager.cpp
)
set(HEADERS
@ -194,6 +203,10 @@ set(HEADERS
src/components/conference/ConferenceModel.hpp
src/components/contact/ContactModel.hpp
src/components/contact/VcardModel.hpp
src/components/contacts/ContactsImporterModel.hpp
src/components/contacts/ContactsImporterPluginsManager.hpp
src/components/contacts/ContactsImporterListModel.hpp
src/components/contacts/ContactsImporterListProxyModel.hpp
src/components/contacts/ContactsListModel.hpp
src/components/contacts/ContactsListProxyModel.hpp
src/components/core/CoreHandlers.hpp
@ -224,8 +237,12 @@ set(HEADERS
src/utils/MediastreamerUtils.hpp
src/utils/QExifImageHeader.hpp
src/utils/Utils.hpp
src/utils/plugins/PluginsManager.hpp
include/LinphoneApp/PluginDataAPI.hpp
include/LinphoneApp/PluginNetworkHelper.hpp
include/LinphoneApp/LinphonePlugin.hpp
)
list(APPEND SOURCES include/LinphoneApp/PluginExample.json)
set(MAIN_FILE src/app/main.cpp)
if (APPLE)
@ -336,8 +353,8 @@ list(APPEND _QML_IMPORT_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/ui/scripts")
list(APPEND _QML_IMPORT_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/ui/views")
set(QML_IMPORT_PATH ${_QML_IMPORT_PATHS} CACHE STRING "Path used to locate CMake modules by Qt Creator" FORCE)
set(QML2_IMPORT_PATH ${_QML_IMPORT_PATHS} CACHE STRING "Path used to locate CMake modules by Qt Creator" FORCE)
set(QML_IMPORT_PATH "${_QML_IMPORT_PATHS}" CACHE STRING "Path used to locate CMake modules by Qt Creator" FORCE)
set(QML2_IMPORT_PATH "${_QML_IMPORT_PATHS}" CACHE STRING "Path used to locate CMake modules by Qt Creator" FORCE)
set(QML_SOURCES_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/ui/")
set(QML_MODULES_PATHS ${QML_SOURCES_PATHS})
if(ENABLE_BUILD_VERBOSE)#useful to copy these Paths to QML previewers
@ -421,9 +438,12 @@ endif()
#add_dependencies(project_b_exe project_a)
#target_link_libraries(project_b_exe ${install_dir}/lib/alib.lib)
if(ENABLE_APP_EXPORT_PLUGIN)
add_definitions(-DENABLE_APP_EXPORT_PLUGIN)
endif()
set(INCLUDED_DIRECTORIES "${LINPHONECXX_INCLUDE_DIRS}" )
list(APPEND INCLUDED_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/include")
set(LIBRARIES_LIST ${BCTOOLBOX_CORE_LIBRARIES} ${BELCARD_LIBRARIES} ${LINPHONE_LIBRARIES} ${LINPHONECXX_LIBRARIES} ${MEDIASTREAMER2_LIBRARIES} ${ORTP_LIBRARIES} ${OPUS_LIBRARIES})
if(WIN32)
set(LIBRARIES)
@ -439,7 +459,6 @@ else()
set(LIBRARIES ${LIBRARIES_LIST})
endif()
if(ENABLE_BUILD_VERBOSE)
message("LIBRARIES : ${LIBRARIES}")
endif()
@ -465,6 +484,9 @@ foreach (package ${QT5_PACKAGES_OPTIONAL})
endif ()
endforeach ()
#find_library(CONTACTS_PLUGIN_LIBRARY linphoneAppContacts HINTS "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}")
#list(APPEND LIBRARIES ${CONTACTS_PLUGIN_LIBRARY})
if (APPLE)
list(APPEND LIBRARIES "-framework Cocoa -framework IOKit -framework AVFoundation")
# -framework linphone") #This doesn't work yet
@ -478,6 +500,7 @@ target_link_libraries(${TARGET_NAME} ${LIBRARIES})
if(WIN32)
target_link_libraries(${TARGET_NAME} wsock32 ws2_32)
endif()
target_compile_definitions(${APP_LIBRARY} PUBLIC ENABLE_APP_EXPORT_PLUGIN)
add_dependencies(${APP_LIBRARY} update_translations ${TARGET_NAME}-git-version)
add_dependencies(${TARGET_NAME} ${APP_LIBRARY})
@ -488,6 +511,10 @@ add_dependencies(${TARGET_NAME} ${APP_LIBRARY})
# ------------------------------------------------------------------------------
set(TOOLS_DIR "${CMAKE_BINARY_DIR}/programs")
set(LINPHONE_BUILDER_SIGNING_IDENTITY ${LINPHONE_BUILDER_SIGNING_IDENTITY})
add_custom_command(TARGET ${TARGET_NAME} PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/include/" "${CMAKE_INSTALL_PREFIX}/include/")
#add_custom_command(TARGET ${TARGET_NAME} PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/include/LinphoneApp/*" "${CMAKE_INSTALL_PREFIX}/include/LinphoneApp/")
#configure_file("${CMAKE_CURRENT_SOURCE_DIR}/include/*" "${CMAKE_INSTALL_PREFIX}/include/LinphoneApp/" COPYONLY)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include" DESTINATION ${CMAKE_INSTALL_PREFIX})
add_subdirectory(build)
add_subdirectory(cmake_builder/linphone_package)
@ -496,7 +523,9 @@ add_subdirectory(cmake_builder/linphone_package)
# ------------------------------------------------------------------------------
# To start better integration into IDE.
# ------------------------------------------------------------------------------
source_group(
"Json" REGULAR_EXPRESSION ".+\.json$"
)
source_group(
"Qml" REGULAR_EXPRESSION ".+\.qml$"
)

View file

@ -1183,6 +1183,10 @@ Klik her: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Logfiler blev uploadet til %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Kontakter</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@ Klicken Sie hier: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Protokolle wurden auf %1 hochgeladen</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Kontakte</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1199,6 +1199,10 @@ Click here: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Logs were uploaded to %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation>Address Book Connector</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@ Haga clic aquí: &lt;a href=&quot;%1&quot;&gt;%1 &lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Los registros se cargaron a %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Contactos</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@ Cliquez ici : &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Les logs ont é uploadé sur %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Contacts</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1182,6 +1182,10 @@ Kattintson ide: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;</translation>
<source>logsMailerSuccess</source>
<translation>A naplókat feltöltötték a %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Névjegyek</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@ Clicca: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>I log sono stati caricati a %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Contatti</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@
<source>logsMailerSuccess</source>
<translation> %1 </translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@ Spustelėkite čia: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Žurnalai buvo įkelti į %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Kontaktai</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1182,6 +1182,10 @@ Clique aqui: &lt;a href=&quot;%1&quot;&gt;%1 &lt;/a&gt;</translation>
<source>logsMailerSuccess</source>
<translation>Os registos foram enviados para %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Contatos</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@
<source>logsMailerSuccess</source>
<translation>Журналы были загружены в %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Контакты</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@ Klicka här: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Loggar laddades upp till %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Kontakter</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@ Buraya tıklayın: &lt;a href=&quot;%1&quot;&gt;%1&lt;/a&gt;
<source>logsMailerSuccess</source>
<translation>Günlükler %1 dosyasına yüklendi</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Kişiler</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@
<source>logsMailerSuccess</source>
<translation>Журнали було вивантажено до %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished">Контакти</translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -1183,6 +1183,10 @@
<source>logsMailerSuccess</source>
<translation> %1</translation>
</message>
<message>
<source>contactsTitle</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SettingsAudio</name>

View file

@ -129,6 +129,7 @@ if (WIN32)
install(FILES ${GRAMMAR_FILES} DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/belr/grammars/" )
install(DIRECTORY "${LINPHONE_OUTPUT_DIR}/${CMAKE_INSTALL_DATAROOTDIR}/images" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}" USE_SOURCE_PERMISSIONS OPTIONAL)
install(DIRECTORY "${LINPHONE_OUTPUT_DIR}/${CMAKE_INSTALL_DATAROOTDIR}/sounds" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}" USE_SOURCE_PERMISSIONS)
install(DIRECTORY "${CMAKE_INSTALL_PREFIX}/plugins/" DESTINATION "plugins" USE_SOURCE_PERMISSIONS OPTIONAL)
install(FILES "${LINPHONE_OUTPUT_DIR}/${CMAKE_INSTALL_DATAROOTDIR}/Linphone/rootca.pem" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/${EXECUTABLE_NAME}/")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../../assets/linphonerc-factory" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/${EXECUTABLE_NAME}")
set(APP_QT_CONF_DPI "0")

View file

@ -0,0 +1,44 @@
#ifndef LINPHONE_APP_PLUGIN_H
#define LINPHONE_APP_PLUGIN_H
#include <QtPlugin>
#include <QObject>
#include <QVersionNumber>
// Overload this class to make a plugin for the address book importer
#if defined(_MSC_VER)
#ifdef ENABLE_APP_EXPORT_PLUGIN
#define LINPHONEAPP_DLL_API __declspec(dllexport)
#else
#define LINPHONEAPP_DLL_API __declspec(dllimport)
#endif
#else
#ifdef ENABLE_APP_EXPORT_PLUGIN
#define LINPHONEAPP_DLL_API __attribute__((visibility("default")))
#else
#define LINPHONEAPP_DLL_API
#endif
#endif
class QPluginLoader;
class PluginDataAPI;
class LinphonePlugin
{
// These macro are an example to use in the custom plugin
//Q_OBJECT
//Q_PLUGIN_METADATA(IID LinphonePlugin_iid FILE "PluginExample.json")// You have to set the Capabilities for your plugin
//Q_INTERFACES(LinphonePlugin)
//-----------------------------------------------------------
public:
virtual ~LinphonePlugin() {}
// Specific to DataAPI. See their section
virtual QString getGUIDescriptionToJson() const = 0;// Describe the GUI to be used for the plugin. Json are in Utf8
virtual PluginDataAPI * createInstance(void* core, QPluginLoader * pluginLoader) = 0;// Create an instance of the plugin in LinphoneAppPluginType.
};
#define LinphonePlugin_iid "linphoneApp.LinphonePlugin/1.0"
Q_DECLARE_INTERFACE(LinphonePlugin, LinphonePlugin_iid)
#endif // LINPHONE_APP_PLUGIN_H

View file

@ -0,0 +1,55 @@
#ifndef LINPHONE_APP_PLUGIN_DATA_H
#define LINPHONE_APP_PLUGIN_DATA_H
#include <QVariantMap>
#ifdef ENABLE_APP_EXPORT_PLUGIN
#include "include/LinphoneApp/LinphonePlugin.hpp"
#else
#include <LinphoneApp/LinphonePlugin.hpp>
#endif
class QPluginLoader;
class LinphonePlugin;
// This class regroup Data interface for importing contacts
class LINPHONEAPP_DLL_API PluginDataAPI : public QObject {
Q_OBJECT
public:
typedef enum{ALL=-1, NOTHING=0, CONTACTS=1, LAST} PluginCapability;// LAST must not be used. It is for internal process only.
PluginDataAPI(LinphonePlugin * plugin, void * linphoneCore, QPluginLoader * pluginLoader);
virtual ~PluginDataAPI();
virtual bool isValid(const bool &pRequestData=true, QString * pError= nullptr) = 0; // Test if the passed data is valid. Used for saving.
virtual void setInputFields(const PluginCapability& capability = ALL, const QVariantMap &inputFields = QVariantMap());// Set all inputs for the selected capability
virtual QMap<PluginCapability, QVariantMap> getInputFields(const PluginCapability& capability);// Get all inputs
virtual QMap<PluginCapability, QVariantMap> getInputFieldsToSave(const PluginCapability& capability = ALL);// Get all inputs to save in config file.
// Configuration management
void setSectionConfiguration(const QString& section);
virtual void loadConfiguration(const PluginCapability& capability = ALL);
virtual void saveConfiguration(const PluginCapability& capability = ALL);
virtual void cleanAllConfigurations();// Remove all saved configuration
QPluginLoader * getPluginLoader();// Used to retrieve the loader that created this instance, in order to unload it
virtual void run(const PluginCapability& actionType)=0;
signals:
void dataReceived(const PluginCapability& actionType, QVector<QMultiMap<QString,QString> > data);
//------------------------------------
void inputFieldsChanged(const PluginCapability&, const QVariantMap &inputFields); // Input fields have been changed
void message(const QtMsgType& type, const QString &message); // Send a message to GUI
protected:
QMap<PluginCapability, QVariantMap> mInputFields;
void * mLinphoneCore;
LinphonePlugin * mPlugin;
QPluginLoader * mPluginLoader;
private:
QString mSectionConfigurationName;
};
#endif // LINPHONE_APP_PLUGIN_DATA_H

View file

@ -0,0 +1,6 @@
{
"Name" : "ExamplePlugin",
"Version" : "1.0.0",
"Capabilities" : "Contacts",
"Description" : "This is an example for describing your plugin. Replace all fields above to be usable by the Application."
}

View file

@ -0,0 +1,40 @@
#ifndef LINPHONE_APP_NETWORK_HELPER_H
#define LINPHONE_APP_NETWORK_HELPER_H
#include <QObject>
#include <QtNetwork>
// This class is used to define network operation to retrieve Addresses from Network
#ifdef ENABLE_APP_EXPORT_PLUGIN
#include "include/LinphoneApp/LinphonePlugin.hpp"
#else
#include <LinphoneApp/LinphonePlugin.hpp>
#endif
class LINPHONEAPP_DLL_API PluginNetworkHelper : public QObject
{
Q_OBJECT
public:
PluginNetworkHelper();
virtual ~PluginNetworkHelper();
virtual QString prepareRequest()const=0; // Called when requesting an Url.
void request();
QPointer<QNetworkReply> mNetworkReply;
QNetworkAccessManager mManager;
signals:
void requestFinished(const QByteArray &data); // The request is over and have data
void message(const QtMsgType &type, const QString &message);
private:
void handleReadyData();
void handleFinished ();
void handleError (QNetworkReply::NetworkError code);
void handleSslErrors (const QList<QSslError> &sslErrors);
QByteArray mBuffer;
};
#endif // LINPHONE_APP_NETWORK_HELPER_H

View file

@ -583,6 +583,7 @@ void App::registerTypes () {
registerType<ConferenceHelperModel>("ConferenceHelperModel");
registerType<ConferenceModel>("ConferenceModel");
registerType<ContactsListProxyModel>("ContactsListProxyModel");
registerType<ContactsImporterListProxyModel>("ContactsImporterListProxyModel");
registerType<FileDownloader>("FileDownloader");
registerType<FileExtractor>("FileExtractor");
registerType<HistoryProxyModel>("HistoryProxyModel");
@ -601,6 +602,7 @@ void App::registerTypes () {
registerUncreatableType<ChatModel>("ChatModel");
registerUncreatableType<ConferenceHelperModel::ConferenceAddModel>("ConferenceAddModel");
registerUncreatableType<ContactModel>("ContactModel");
registerUncreatableType<ContactsImporterModel>("ContactsImporterModel");
registerUncreatableType<HistoryModel>("HistoryModel");
registerUncreatableType<SipAddressObserver>("SipAddressObserver");
registerUncreatableType<VcardModel>("VcardModel");
@ -616,6 +618,7 @@ void App::registerSharedTypes () {
registerSharedSingletonType<SipAddressesModel, &CoreManager::getSipAddressesModel>("SipAddressesModel");
registerSharedSingletonType<CallsListModel, &CoreManager::getCallsListModel>("CallsListModel");
registerSharedSingletonType<ContactsListModel, &CoreManager::getContactsListModel>("ContactsListModel");
registerSharedSingletonType<ContactsImporterListModel, &CoreManager::getContactsImporterListModel>("ContactsImporterListModel");
}
void App::registerToolTypes () {
@ -625,6 +628,7 @@ void App::registerToolTypes () {
registerToolType<DesktopTools>("DesktopTools");
registerToolType<TextToSpeech>("TextToSpeech");
registerToolType<Units>("Units");
registerToolType<ContactsImporterPluginsManager>("ContactsImporterPluginsManager");
}
void App::registerSharedToolTypes () {

View file

@ -41,7 +41,12 @@ namespace {
constexpr char PathCodecs[] = "/codecs/";
constexpr char PathTools[] = "/tools/";
constexpr char PathLogs[] = "/logs/";
//constexpr char PathPlugins[] = "/plugins/"; // Unused
#ifdef APPLE
constexpr char PathPlugins[] = "/Plugins/";
#else
constexpr char PathPlugins[] = "/plugins/";
#endif
constexpr char PathPluginsApp[] = "app/";
constexpr char PathThumbnails[] = "/thumbnails/";
constexpr char PathUserCertificates[] = "/usr-crt/";
@ -159,6 +164,10 @@ static inline QString getAppPackageMsPluginsDirPath () {
return dir.absolutePath();
}
static inline QString getAppPackagePluginsDirPath () {
return getAppPackageDir().absolutePath() + PathPlugins;
}
static inline QString getAppAssistantConfigDirPath () {
return getAppPackageDataDirPath() + PathAssistantConfig;
}
@ -187,6 +196,9 @@ static inline QString getAppMessageHistoryFilePath () {
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + PathMessageHistoryList;
}
static inline QString getAppPluginsDirPath () {
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)+ PathPlugins;
}
// -----------------------------------------------------------------------------
bool Paths::filePathExists (const string &path) {
@ -265,6 +277,21 @@ string Paths::getPackageMsPluginsDirPath () {
return getReadableDirPath(getAppPackageMsPluginsDirPath());
}
string Paths::getPackagePluginsAppDirPath () {
return getReadableDirPath(getAppPackagePluginsDirPath()+PathPluginsApp);
}
string Paths::getPluginsAppDirPath () {
return getWritableDirPath(getAppPluginsDirPath()+PathPluginsApp);
}
QStringList Paths::getPluginsAppFolders() {
QStringList pluginPaths;
pluginPaths << Utils::coreStringToAppString(Paths::getPluginsAppDirPath());
pluginPaths << Utils::coreStringToAppString(Paths::getPackagePluginsAppDirPath());
return pluginPaths;
}
string Paths::getRootCaFilePath () {
return getReadableFilePath(getAppRootCaFilePath());
}

View file

@ -28,6 +28,7 @@
namespace Paths {
bool filePathExists (const std::string &path);
std::string getAssistantConfigDirPath ();
std::string getAvatarsDirPath ();
std::string getCallHistoryFilePath ();
@ -42,6 +43,9 @@ namespace Paths {
std::string getMessageHistoryFilePath ();
std::string getPackageDataDirPath ();
std::string getPackageMsPluginsDirPath ();
std::string getPackagePluginsAppDirPath ();
std::string getPluginsAppDirPath ();
QStringList getPluginsAppFolders();
std::string getRootCaFilePath ();
std::string getThumbnailsDirPath ();
std::string getToolsDirPath ();

View file

@ -37,6 +37,10 @@
#include "contact/VcardModel.hpp"
#include "contacts/ContactsListModel.hpp"
#include "contacts/ContactsListProxyModel.hpp"
#include "contacts/ContactsImporterModel.hpp"
#include "contacts/ContactsImporterPluginsManager.hpp"
#include "contacts/ContactsImporterListModel.hpp"
#include "contacts/ContactsImporterListProxyModel.hpp"
#include "core/CoreHandlers.hpp"
#include "core/CoreManager.hpp"
#include "file/FileDownloader.hpp"

View file

@ -0,0 +1,209 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QQmlApplicationEngine>
#include "app/App.hpp"
#include "ContactsImporterModel.hpp"
#include "ContactsImporterListModel.hpp"
#include "ContactsImporterPluginsManager.hpp"
#include "components/core/CoreManager.hpp"
#include "utils/Utils.hpp"
// =============================================================================
using namespace std;
ContactsImporterListModel::ContactsImporterListModel (QObject *parent) : QAbstractListModel(parent) {
// Init contacts with linphone friends list.
mMaxContactsImporterId = -1;
QQmlEngine *engine = App::getInstance()->getEngine();
auto config = CoreManager::getInstance()->getCore()->getConfig();
PluginsManager::getPlugins();// Initialize list
// Read configuration file
std::list<std::string> sections = config->getSectionsNamesList();
for(auto section : sections){
QString qtSection = Utils::coreStringToAppString(section);
QStringList parse = qtSection.split("_");// PluginsManager::gPluginsConfigSection_id_capability
if( parse.size() > 2){
QVariantMap importData;
if( parse[2].toInt() == PluginDataAPI::CONTACTS){// We only care about Contacts
int id = parse[1].toInt();
mMaxContactsImporterId = qMax(id, mMaxContactsImporterId);
std::list<std::string> keys = config->getKeysNamesList(section);
auto keyName = std::find(keys.begin(), keys.end(), "pluginID");
if( keyName != keys.end()){
QString pluginID = Utils::coreStringToAppString(config->getString(section, *keyName, ""));
PluginDataAPI* data = static_cast<PluginDataAPI*>(PluginsManager::createInstance(pluginID));
if(data) {
ContactsImporterModel * model = new ContactsImporterModel(data, this);
// See: http://doc.qt.io/qt-5/qtqml-cppintegration-data.html#data-ownership
// The returned value must have a explicit parent or a QQmlEngine::CppOwnership.
engine->setObjectOwnership(model, QQmlEngine::CppOwnership);
model->setIdentity(id);
model->loadConfiguration();// Read the configuration contacts inside the plugin
addContactsImporter(model);
}
}
}
}
}
}
// GUI methods
int ContactsImporterListModel::rowCount (const QModelIndex &) const {
return mList.count();
}
QHash<int, QByteArray> ContactsImporterListModel::roleNames () const {
QHash<int, QByteArray> roles;
roles[Qt::DisplayRole] = "$contactsImporter";
return roles;
}
QVariant ContactsImporterListModel::data (const QModelIndex &index, int role) const {
int row = index.row();
if (!index.isValid() || row < 0 || row >= mList.count())
return QVariant();
if (role == Qt::DisplayRole)
return QVariant::fromValue(mList[row]);
return QVariant();
}
bool ContactsImporterListModel::removeRow (int row, const QModelIndex &parent) {
return removeRows(row, 1, parent);
}
bool ContactsImporterListModel::removeRows (int row, int count, const QModelIndex &parent) {
int limit = row + count - 1;
if (row < 0 || count < 0 || limit >= mList.count())
return false;
beginRemoveRows(parent, row, limit);
for (int i = 0; i < count; ++i) {
ContactsImporterModel *contactsImporter = dynamic_cast<ContactsImporterModel*>(mList.takeAt(row));
emit contactsImporterRemoved(contactsImporter);
contactsImporter->deleteLater();
}
endRemoveRows();
return true;
}
// -----------------------------------------------------------------------------
ContactsImporterModel *ContactsImporterListModel::findContactsImporterModelFromId (const int &id) const {
auto it = find_if(mList.begin(), mList.end(), [id](PluginsModel *contactsImporterModel) {
return contactsImporterModel->getIdentity() == id;
});
return it != mList.end() ? dynamic_cast<ContactsImporterModel*>(*it) : nullptr;
}
QList<PluginsModel*> ContactsImporterListModel::getList(){
return mList;
}
// -----------------------------------------------------------------------------
ContactsImporterModel *ContactsImporterListModel::createContactsImporter(QVariantMap data){
ContactsImporterModel *contactsImporter = nullptr;
if( data.contains("pluginID")){
PluginDataAPI * dataInstance = static_cast<PluginDataAPI*>(PluginsManager::createInstance(data["pluginID"].toString()));
if(dataInstance) {
// get default values
contactsImporter = new ContactsImporterModel(dataInstance, this);
App::getInstance()->getEngine()->setObjectOwnership(contactsImporter, QQmlEngine::CppOwnership);
QVariantMap newData = ContactsImporterPluginsManager::getDefaultValues(data["pluginID"].toString());// Start with defaults from plugin
QVariantMap InstanceFields = contactsImporter->getFields();
for(auto field = InstanceFields.begin() ; field != InstanceFields.end() ; ++field)// Insert or Update with the defaults of an instance
newData[field.key()] = field.value();
for(auto field = data.begin() ; field != data.end() ; ++field)// Insert or Update with Application data
newData[field.key()] = field.value();
contactsImporter->setIdentity(++mMaxContactsImporterId);
contactsImporter->setFields(newData);
int row = mList.count();
beginInsertRows(QModelIndex(), row, row);
addContactsImporter(contactsImporter);
endInsertRows();
emit contactsImporterAdded(contactsImporter);
}
}
return contactsImporter;
}
ContactsImporterModel *ContactsImporterListModel::addContactsImporter (QVariantMap data, int pId) {
ContactsImporterModel *contactsImporter = findContactsImporterModelFromId(pId);
if (contactsImporter) {
contactsImporter->setFields(data);
return contactsImporter;
}else
return createContactsImporter(data);
}
void ContactsImporterListModel::removeContactsImporter (ContactsImporterModel *contactsImporter) {
int index = mList.indexOf(contactsImporter);
if (index >=0){
if( contactsImporter->getIdentity() >=0 ){// Remove from configuration
int id = contactsImporter->getIdentity();
string section = Utils::appStringToCoreString(PluginsManager::gPluginsConfigSection+"_"+QString::number(id)+"_"+QString::number(PluginDataAPI::CONTACTS));
CoreManager::getInstance()->getCore()->getConfig()->cleanSection(section);
if( id == mMaxContactsImporterId)// Decrease mMaxContactsImporterId in a safe way
--mMaxContactsImporterId;
}
removeRow(index);
}
}
void ContactsImporterListModel::importContacts(const int &pId){
if( pId >=0) {
ContactsImporterModel *contactsImporter = findContactsImporterModelFromId(pId);
if( contactsImporter)
contactsImporter->importContacts();
}else // Import from all current connectors
for(auto importer : mList)
dynamic_cast<ContactsImporterModel*>(importer)->importContacts();
}
// -----------------------------------------------------------------------------
void ContactsImporterListModel::addContactsImporter (ContactsImporterModel *contactsImporter) {
// Connect all update signals
QObject::connect(contactsImporter, &ContactsImporterModel::fieldsChanged, this, [this, contactsImporter]() {
emit contactsImporterUpdated(contactsImporter);
});
QObject::connect(contactsImporter, &ContactsImporterModel::identityChanged, this, [this, contactsImporter]() {
emit contactsImporterUpdated(contactsImporter);
});
mList << contactsImporter;
}
//-----------------------------------------------------------------------------------

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTS_IMPORTER_LIST_MODEL_H_
#define CONTACTS_IMPORTER_LIST_MODEL_H_
#include <memory>
#include <QAbstractListModel>
// =============================================================================
class ContactsImporterModel;
class PluginsModel;
// Store all connectors
class ContactsImporterListModel : public QAbstractListModel {
Q_OBJECT;
public:
ContactsImporterListModel (QObject *parent = Q_NULLPTR);
int rowCount (const QModelIndex &index = QModelIndex()) const override;
QHash<int, QByteArray> roleNames () const override;
QVariant data (const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool removeRow (int row, const QModelIndex &parent = QModelIndex());
bool removeRows (int row, int count, const QModelIndex &parent = QModelIndex()) override;
ContactsImporterModel *findContactsImporterModelFromId (const int &id) const;
QList<PluginsModel*> getList();
Q_INVOKABLE ContactsImporterModel *createContactsImporter(QVariantMap data);
Q_INVOKABLE ContactsImporterModel *addContactsImporter (QVariantMap data, int id=-1);
Q_INVOKABLE void removeContactsImporter (ContactsImporterModel *importer);
Q_INVOKABLE void importContacts(const int &id = -1); // Import contacts for all enabled importer if -1
//-----------------------------------------------------------------------------------
signals:
void contactsImporterAdded (ContactsImporterModel *contact);
void contactsImporterRemoved (const ContactsImporterModel *contact);
void contactsImporterUpdated (ContactsImporterModel *contact);
private:
void addContactsImporter (ContactsImporterModel *contactsImporter);
QList<PluginsModel *> mList;
int mMaxContactsImporterId; // Used to ensure unicity on ID when creating a connector
};
#endif // CONTACTS_IMPORTER_LIST_MODEL_H_

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cmath>
#include "components/contacts/ContactsImporterModel.hpp"
#include "components/core/CoreManager.hpp"
#include "utils/Utils.hpp"
#include "ContactsImporterListModel.hpp"
#include "ContactsImporterListProxyModel.hpp"
// =============================================================================
using namespace std;
// -----------------------------------------------------------------------------
ContactsImporterListProxyModel::ContactsImporterListProxyModel (QObject *parent) : QSortFilterProxyModel(parent) {
setSourceModel(CoreManager::getInstance()->getContactsImporterListModel());
sort(0);// Sort by identity
}
// -----------------------------------------------------------------------------
bool ContactsImporterListProxyModel::filterAcceptsRow (
int sourceRow,
const QModelIndex &sourceParent
) const {
Q_UNUSED(sourceRow)
Q_UNUSED(sourceParent)
return true;
}
bool ContactsImporterListProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const {
const ContactsImporterModel *contactA = sourceModel()->data(left).value<ContactsImporterModel *>();
const ContactsImporterModel *contactB = sourceModel()->data(right).value<ContactsImporterModel *>();
return contactA->getIdentity() <= contactB->getIdentity();
}
// -----------------------------------------------------------------------------

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTS_IMPORTER_LIST_PROXY_MODEL_H_
#define CONTACTS_IMPORTER_LIST_PROXY_MODEL_H_
#include <QSortFilterProxyModel>
// =============================================================================
class ContactsImporterModel;
class ContactsImporterListModel;
// Manage the list of connectors
class ContactsImporterListProxyModel : public QSortFilterProxyModel {
Q_OBJECT;
public:
ContactsImporterListProxyModel (QObject *parent = Q_NULLPTR);
protected:
bool filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const override;
bool lessThan (const QModelIndex &left, const QModelIndex &right) const override;
};
#endif // CONTACTS_IMPORTER_LIST_PROXY_MODEL_H_

View file

@ -0,0 +1,129 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ContactsImporterModel.hpp"
#include "ContactsImporterPluginsManager.hpp"
#include "../../utils/Utils.hpp"
//#include <linphoneapp/contacts/ContactsImporterDataAPI.hpp>
#include "include/LinphoneApp/PluginDataAPI.hpp"
#include <QPluginLoader>
#include <QDebug>
// =============================================================================
using namespace std;
ContactsImporterModel::ContactsImporterModel (PluginDataAPI * data, QObject *parent) : PluginsModel(parent) {
mIdentity = -1;
mData = nullptr;
setDataAPI(data);
}
// -----------------------------------------------------------------------------
void ContactsImporterModel::setDataAPI(PluginDataAPI *data){
if(mData){// Unload the current plugin loader and delete it from memory
QPluginLoader * loader = mData->getPluginLoader();
delete mData;
if(loader){
loader->unload();
delete loader;
}
mData = data;
}else
mData = data;
if( mData){
connect(mData, &PluginDataAPI::inputFieldsChanged, this, &ContactsImporterModel::fieldsChanged);
connect(mData, &PluginDataAPI::message, this, &ContactsImporterModel::messageReceived);
connect(mData, &PluginDataAPI::dataReceived, this, &ContactsImporterModel::parsedContacts);
}
}
PluginDataAPI *ContactsImporterModel::getDataAPI(){
return mData;
}
bool ContactsImporterModel::isUsable(){
if( mData){
if( !mData->getPluginLoader()->isLoaded())
mData->getPluginLoader()->load();
return mData->getPluginLoader()->isLoaded();
}else
return false;
}
QVariantMap ContactsImporterModel::getFields(){
return (isUsable()?mData->getInputFields(PluginDataAPI::CONTACTS)[PluginDataAPI::CONTACTS] :QVariantMap());
}
void ContactsImporterModel::setFields(const QVariantMap &pFields){
if( isUsable())
mData->setInputFields(PluginDataAPI::CONTACTS, pFields);
}
int ContactsImporterModel::getIdentity()const{
return mIdentity;
}
void ContactsImporterModel::setIdentity(const int &pIdentity){
if( mIdentity != pIdentity){
mIdentity = pIdentity;
if(mData && mData->getPluginLoader()->isLoaded())
mData->setSectionConfiguration(PluginsManager::gPluginsConfigSection+"_"+QString::number(mIdentity));
emit identityChanged(mIdentity);
}
}
void ContactsImporterModel::loadConfiguration(){
if(isUsable())
mData->loadConfiguration(PluginDataAPI::CONTACTS);
}
void ContactsImporterModel::importContacts(){
if(isUsable()){
qInfo() << "Importing contacts with " << mData->getInputFields(PluginDataAPI::CONTACTS)[PluginDataAPI::CONTACTS]["pluginTitle"];
QPluginLoader * loader = mData->getPluginLoader();
if( !loader)
qWarning() << "Loader is NULL";
else{
qWarning() << "Plugin loaded Status : " << loader->isLoaded() << " for " << loader->fileName();
}
mData->run(PluginDataAPI::CONTACTS);
}else
qWarning() << "Cannot import contacts, mData is NULL or plugin cannot be loaded ";
}
void ContactsImporterModel::parsedContacts(const PluginDataAPI::PluginCapability& actionType, QVector<QMultiMap<QString, QString> > contacts){
if(actionType == PluginDataAPI::CONTACTS)
ContactsImporterPluginsManager::importContacts(contacts);
}
void ContactsImporterModel::updateInputs(const PluginDataAPI::PluginCapability& capability, const QVariantMap &inputs){
if(capability == PluginDataAPI::CONTACTS)
setFields(inputs);
}
void ContactsImporterModel::messageReceived(const QtMsgType& type, const QString &message){
if( type == QtMsgType::QtInfoMsg)
emit statusMessage(message);
else
emit errorMessage(message);
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTS_IMPORTER_MODEL_H_
#define CONTACTS_IMPORTER_MODEL_H_
#include <QObject>
#include <QVariantMap>
#include "utils/plugins/PluginsManager.hpp"
#include "include/LinphoneApp/PluginDataAPI.hpp"
// =============================================================================
class ContactsImporterModel : public PluginsModel {
Q_OBJECT
Q_PROPERTY(QVariantMap fields READ getFields WRITE setFields NOTIFY fieldsChanged)
Q_PROPERTY(int identity READ getIdentity WRITE setIdentity NOTIFY identityChanged)
public:
ContactsImporterModel (PluginDataAPI * data, QObject *parent = nullptr);
void setDataAPI(PluginDataAPI *data);
PluginDataAPI *getDataAPI();
bool isUsable(); // Return true if the plugin can be load and has been loaded.
QVariantMap getFields();
void setFields(const QVariantMap &pFields);
int getIdentity()const;
void setIdentity(const int &pIdentity);
void loadConfiguration();
Q_INVOKABLE void importContacts();
public slots:
void parsedContacts(const PluginDataAPI::PluginCapability& actionType, QVector<QMultiMap<QString, QString> > contacts);
void updateInputs(const PluginDataAPI::PluginCapability&, const QVariantMap &inputs);
void messageReceived(const QtMsgType& type, const QString &message);
signals:
void fieldsChanged (const PluginDataAPI::PluginCapability&, QVariantMap fields);
void identityChanged(int identity);
void errorMessage(const QString& message);
void statusMessage(const QString& message);
private:
int mIdentity; // The identity of the model in configuration. It must be unique between all contact plugins.
PluginDataAPI *mData; // The instance of the plugin with its plugin Loader.
};
Q_DECLARE_METATYPE(ContactsImporterModel *);
#endif // CONTACTS_IMPORTER_MODEL_H_

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ContactsImporterPluginsManager.hpp"
#include "ContactsImporterModel.hpp"
#include "include/LinphoneApp/PluginNetworkHelper.hpp"
#include "utils/Utils.hpp"
#include "app/paths/Paths.hpp"
#include "components/contact/VcardModel.hpp"
#include "components/contacts/ContactsListModel.hpp"
#include "components/contacts/ContactsImporterListModel.hpp"
#include "components/core/CoreManager.hpp"
#include "components/sip-addresses/SipAddressesModel.hpp"
#include <QDir>
#include <QPluginLoader>
#include <QDebug>
#include <QJsonDocument>
#include <QFileDialog>
#include <QMessageBox>
// =============================================================================
ContactsImporterPluginsManager::ContactsImporterPluginsManager(QObject * parent) : PluginsManager(parent){
}
QVariantMap ContactsImporterPluginsManager::getContactsImporterPluginDescription(const QString& pluginID) {
QVariantMap description;
QJsonDocument doc = getJson(pluginID);
description = doc.toVariant().toMap();
if(description.contains("fields")){
auto fields = description["fields"].toList();
auto removedFields = std::remove_if(fields.begin(), fields.end(),
[](const QVariant& f){
auto field = f.toMap();
return field.contains("capability") && ((field["capability"].toInt() & PluginDataAPI::CONTACTS) != PluginDataAPI::CONTACTS);
});
fields.erase(removedFields, fields.end());
description["fields"] = fields;
}
return description;
}
void ContactsImporterPluginsManager::openNewPlugin(){
PluginsManager::openNewPlugin("Import Address Book Connector");
}
QVariantList ContactsImporterPluginsManager::getPlugins(){
return PluginsManager::getPlugins(PluginDataAPI::CONTACTS);
}
void ContactsImporterPluginsManager::importContacts(ContactsImporterModel * model) {
if(model){
QString pluginID = model->getFields()["pluginID"].toString();
if(!pluginID.isEmpty()){
if( !PluginsManager::gPluginsMap.contains(pluginID))
qInfo() << "Unknown " << pluginID;
model->importContacts();
}else
qWarning() << "Error : Cannot import contacts : pluginID is empty";
}
}
void ContactsImporterPluginsManager::importContacts(const QVector<QMultiMap<QString, QString> >& pContacts ){
for(int i = 0 ; i < pContacts.size() ; ++i){
VcardModel * card = CoreManager::getInstance()->createDetachedVcardModel();
SipAddressesModel * sipConvertion = CoreManager::getInstance()->getSipAddressesModel();
QString domain = pContacts[i].values("sipDomain").at(0);
//if(pContacts[i].contains("phoneNumber"))
// card->addSipAddress(sipConvertion->interpretSipAddress(pContacts[i].values("phoneNumber").at(0)+"@"+domain, false));
if(pContacts[i].contains("displayName") && pContacts[i].values("displayName").size() > 0)
card->setUsername(pContacts[i].values("displayName").at(0));
if(pContacts[i].contains("sipUsername") && pContacts[i].values("sipUsername").size() > 0){
QString sipUsername = pContacts[i].values("sipUsername").at(0);
QString convertedUsername = sipConvertion->interpretSipAddress(sipUsername, domain);
if(!convertedUsername.contains(domain)){
convertedUsername = convertedUsername.replace('@',"%40")+"@"+domain;
}
card->addSipAddress(convertedUsername);
if( sipUsername.contains('@')){
card->addEmail(sipUsername);
}
}
if(pContacts[i].contains("email"))
for(auto email : pContacts[i].values("email"))
card->addEmail(email);
if(pContacts[i].contains("organization"))
for(auto company : pContacts[i].values("organization"))
card->addCompany(company);
if( card->getSipAddresses().size()>0){
CoreManager::getInstance()->getContactsListModel()->addContact(card);
}else
delete card;
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTS_IMPORTER_PLUGINS_MANAGER_MODEL_H_
#define CONTACTS_IMPORTER_PLUGINS_MANAGER_MODEL_H_
#include <QObject>
#include <QVariantList>
// =============================================================================
#include "utils/plugins/PluginsManager.hpp"
class ContactsImporterModel;
class PluginContactsDataAPI;
class QPluginLoader;
class ContactsImporterPluginsManager : public PluginsManager{
Q_OBJECT
public:
ContactsImporterPluginsManager (QObject *parent = Q_NULLPTR);
Q_INVOKABLE static void openNewPlugin(); // Open a File Dialog. Test if the file can be load and have a matched version. Replace old plugins from custom paths and with the same plugin title.
Q_INVOKABLE static QVariantList getPlugins(); // Get a list of all available plugins
Q_INVOKABLE static QVariantMap getContactsImporterPluginDescription(const QString& pluginID); // Get the description of the plugin. It is used for GUI to create dynamically items
Q_INVOKABLE static void importContacts(ContactsImporterModel * model); // Request the import of the model
static void importContacts(const QVector<QMultiMap<QString, QString> >& contacts ); // Merge these data into contacts
};
#endif // CONTACTS_IMPORTER_PLUGINS_MANAGER_MODEL_H_

View file

@ -32,10 +32,12 @@
#include "components/chat/ChatModel.hpp"
#include "components/contact/VcardModel.hpp"
#include "components/contacts/ContactsListModel.hpp"
#include "components/contacts/ContactsImporterListModel.hpp"
#include "components/history/HistoryModel.hpp"
#include "components/settings/AccountSettingsModel.hpp"
#include "components/settings/SettingsModel.hpp"
#include "components/sip-addresses/SipAddressesModel.hpp"
#include "utils/Utils.hpp"
#if defined(Q_OS_MACOS)
@ -46,6 +48,7 @@
#include "CoreHandlers.hpp"
#include "CoreManager.hpp"
#include <linphone/core.h>
#include <linphone/core.h>
@ -78,7 +81,6 @@ CoreManager::CoreManager (QObject *parent, const QString &configPath) :
QObject::connect(coreHandlers, &CoreHandlers::coreStarted, this, &CoreManager::initCoreManager, Qt::QueuedConnection);
QObject::connect(coreHandlers, &CoreHandlers::coreStopped, this, &CoreManager::stopIterate, Qt::QueuedConnection);
QObject::connect(coreHandlers, &CoreHandlers::logsUploadStateChanged, this, &CoreManager::handleLogsUploadStateChanged);
createLinphoneCore(configPath);
}
@ -93,6 +95,7 @@ CoreManager::~CoreManager(){
void CoreManager::initCoreManager(){
mCallsListModel = new CallsListModel(this);
mContactsListModel = new ContactsListModel(this);
mContactsImporterListModel = new ContactsImporterListModel(this);
mAccountSettingsModel = new AccountSettingsModel(this);
mSettingsModel = new SettingsModel(this);
mSipAddressesModel = new SipAddressesModel(this);
@ -225,8 +228,7 @@ void CoreManager::setDatabasesPaths () {
SET_DATABASE_PATH(Friends, Paths::getFriendsListFilePath());
SET_DATABASE_PATH(CallLogs, Paths::getCallHistoryFilePath());
if(QFile::exists(Utils::coreStringToAppString(Paths::getMessageHistoryFilePath()))){
linphone_core_set_chat_database_path(mCore->cPtr(), Paths::getMessageHistoryFilePath().c_str());
//SET_DATABASE_PATH(Chat, Paths::getMessageHistoryFilePath());// Setting the message database let SDK to migrate data
linphone_core_set_chat_database_path(mCore->cPtr(), Paths::getMessageHistoryFilePath().c_str());// Setting the message database let SDK to migrate data
QFile::remove(Utils::coreStringToAppString(Paths::getMessageHistoryFilePath()));
}
}

View file

@ -35,6 +35,7 @@ class AccountSettingsModel;
class CallsListModel;
class ChatModel;
class ContactsListModel;
class ContactsImporterListModel;
class CoreHandlers;
class EventCountNotifier;
class HistoryModel;
@ -93,6 +94,13 @@ public:
Q_CHECK_PTR(mContactsListModel);
return mContactsListModel;
}
ContactsImporterListModel *getContactsImporterListModel () const {
Q_CHECK_PTR(mContactsImporterListModel);
return mContactsImporterListModel;
}
SipAddressesModel *getSipAddressesModel () const {
Q_CHECK_PTR(mSipAddressesModel);
@ -180,6 +188,8 @@ private:
CallsListModel *mCallsListModel = nullptr;
ContactsListModel *mContactsListModel = nullptr;
ContactsImporterListModel *mContactsImporterListModel = nullptr;
SipAddressesModel *mSipAddressesModel = nullptr;
SettingsModel *mSettingsModel = nullptr;
AccountSettingsModel *mAccountSettingsModel = nullptr;

View file

@ -20,6 +20,8 @@
#include <QDir>
#include <QtDebug>
#include <QPluginLoader>
#include <QJsonDocument>
#include <cstdlib>
#include <cmath>
@ -27,6 +29,7 @@
#include "app/logger/Logger.hpp"
#include "app/paths/Paths.hpp"
#include "components/core/CoreManager.hpp"
#include "include/LinphoneApp/PluginNetworkHelper.hpp"
#include "utils/Utils.hpp"
#include "utils/MediastreamerUtils.hpp"
#include "SettingsModel.hpp"
@ -41,6 +44,7 @@ namespace {
}
const string SettingsModel::UiSection("ui");
const string SettingsModel::ContactsSection("contacts_import");
SettingsModel::SettingsModel (QObject *parent) : QObject(parent) {
CoreManager *coreManager = CoreManager::getInstance();
@ -102,6 +106,7 @@ void SettingsModel::onSettingsTabChanged(int idx) {
case 4://ui
break;
case 5://advanced
accessAdvancedSettings();
break;
default:
break;
@ -1187,6 +1192,12 @@ void SettingsModel::setExitOnClose (bool value) {
// Advanced.
// =============================================================================
void SettingsModel::accessAdvancedSettings() {
emit contactImporterChanged();
}
//------------------------------------------------------------------------------
QString SettingsModel::getLogsFolder () const {
return getLogsFolder(mConfig);
}
@ -1263,7 +1274,6 @@ bool SettingsModel::getLogsEnabled (const shared_ptr<linphone::Config> &config)
}
// ---------------------------------------------------------------------------
bool SettingsModel::getDeveloperSettingsEnabled () const {
#ifdef DEBUG
return !!mConfig->getInt(UiSection, "developer_settings", 0);

View file

@ -27,6 +27,7 @@
#include <QVariantMap>
#include "components/core/CoreHandlers.hpp"
#include "components/contacts/ContactsImporterModel.hpp"
// =============================================================================
@ -425,7 +426,10 @@ public:
bool getExitOnClose () const;
void setExitOnClose (bool value);
// ---------------------------------------------------------------------------
// Advanced. ---------------------------------------------------------------------------
void accessAdvancedSettings();
QString getLogsFolder () const;
void setLogsFolder (const QString &folder);
@ -456,6 +460,7 @@ public:
bool getIsInCall() const;
static const std::string UiSection;
static const std::string ContactsSection;
// ===========================================================================
// SIGNALS.
@ -532,7 +537,7 @@ signals:
void fileTransferUrlChanged (const QString &url);
void mediaEncryptionChanged (MediaEncryption encryption);
void limeStateChanged (bool state);
void limeStateChanged (bool state);
void contactsEnabledChanged (bool status);
@ -586,6 +591,8 @@ signals:
void logsUploadUrlChanged (const QString &url);
void logsEnabledChanged (bool status);
void logsEmailChanged (const QString &email);
void contactImporterChanged();
bool developerSettingsEnabledChanged (bool status);

View file

@ -196,6 +196,32 @@ QString SipAddressesModel::interpretSipAddress (const QString &sipAddress, bool
return Utils::coreStringToAppString(lAddress->asStringUriOnly());
return QString("");
}
QString SipAddressesModel::interpretSipAddress (const QString &sipAddress, const QString &domain) {
auto core = CoreManager::getInstance()->getCore();
if(!core){
qWarning() << "No core to interpret address";
}else{
auto proxyConfig = CoreManager::getInstance()->getCore()->createProxyConfig();
if( !proxyConfig) {
}else{
shared_ptr<linphone::Address> lAddressTemp = core->createPrimaryContactParsed();// Create an address
if( lAddressTemp ){
lAddressTemp->setDomain(Utils::appStringToCoreString(domain)); // Set the domain and use the address into proxy
proxyConfig->setIdentityAddress(lAddressTemp);
shared_ptr<linphone::Address> lAddress = proxyConfig->normalizeSipUri(Utils::appStringToCoreString(sipAddress));
if (lAddress) {
return Utils::coreStringToAppString(lAddress->asStringUriOnly());
} else {
qWarning() << "Cannot normalize Sip Uri : " << sipAddress << " / " << domain;
return QString("");
}
}else{
qWarning() << "Cannot create a Primary Contact Parsed";
}
}
}
return QString("");
}
QString SipAddressesModel::interpretSipAddress (const QUrl &sipAddress) {
return sipAddress.toString();

View file

@ -74,6 +74,7 @@ public:
Q_INVOKABLE static QString interpretSipAddress (const QString &sipAddress, bool checkUsername = true);
Q_INVOKABLE static QString interpretSipAddress (const QUrl &sipAddress);
Q_INVOKABLE static QString interpretSipAddress (const QString &sipAddress, const QString &domain);
Q_INVOKABLE static bool addressIsValid (const QString &address);
Q_INVOKABLE static bool sipAddressIsValid (const QString &sipAddress);

View file

@ -0,0 +1 @@
#include "include/LinphoneApp/LinphonePlugin.hpp"

View file

@ -0,0 +1,118 @@
#include "include/LinphoneApp/PluginDataAPI.hpp"
#include <linphone++/core.hh>
#include <linphone++/config.hh>
#include <QVariantMap>
#include <QJsonDocument>
#include <QPluginLoader>
// This class regroup Data interface for importing contacts
#include "include/LinphoneApp/LinphonePlugin.hpp"
PluginDataAPI::PluginDataAPI(LinphonePlugin * plugin, void* linphoneCore, QPluginLoader * pluginLoader) : mPlugin(plugin), mLinphoneCore(linphoneCore), mPluginLoader(pluginLoader){
QVariantMap defaultValues;
QJsonDocument doc = QJsonDocument::fromJson(mPlugin->getGUIDescriptionToJson().toUtf8());
QVariantMap description = doc.toVariant().toMap();
mPluginLoader->setLoadHints(0);
// First, get all fields where their target is ALL. It will be act as a "default field"
for(auto field : description["fields"].toList()){
auto details = field.toMap();
if( details.contains("fieldId") && details.contains("defaultData")){
int fieldCapability = details["capability"].toInt();
if( fieldCapability == PluginCapability::ALL){
for(int capability = PluginCapability::CONTACTS ; capability != PluginCapability::LAST ; ++capability){
mInputFields[static_cast<PluginCapability>(capability)][details["fieldId"].toString()] = details["defaultData"].toString();
}
}
}
}
// Second, get all fields that are not for ALL and add them
for(auto field : description["fields"].toList()){
auto details = field.toMap();
if( details.contains("fieldId") && details.contains("defaultData")){
int fieldCapability = details["capability"].toInt();
if( fieldCapability> PluginCapability::NOTHING)
mInputFields[static_cast<PluginCapability>(fieldCapability)][details["fieldId"].toString()] = details["defaultData"].toString();
}
}
for(auto inputFields : mInputFields)
inputFields["enabled"] = 0;
}
PluginDataAPI::~PluginDataAPI(){
}
void PluginDataAPI::setInputFields(const PluginCapability& pCapability, const QVariantMap &inputFields){
for(int capabilityIndex = (pCapability == PluginCapability::ALL?PluginCapability::CONTACTS:pCapability); capabilityIndex != (pCapability == PluginCapability::ALL?PluginCapability::LAST:pCapability+1) ; ++capabilityIndex){
PluginCapability selectedCapability = static_cast<PluginCapability>(capabilityIndex);
if(mInputFields[selectedCapability] != inputFields) {
mInputFields[selectedCapability] = inputFields;
if( isValid(false))
saveConfiguration(selectedCapability);
emit inputFieldsChanged(selectedCapability, mInputFields[selectedCapability]);
}
}
}
QMap<PluginDataAPI::PluginCapability, QVariantMap> PluginDataAPI::getInputFields(const PluginCapability& capability){
if( capability == PluginCapability::ALL)
return mInputFields;
else{
QMap<PluginDataAPI::PluginCapability, QVariantMap> data;
data[capability] = mInputFields[capability];
return data;
}
}
QMap<PluginDataAPI::PluginCapability, QVariantMap> PluginDataAPI::getInputFieldsToSave(const PluginCapability& capability) {
return getInputFields(capability);
}
//----------------------------- CONFIGURATION ---------------------------------------
void PluginDataAPI::setSectionConfiguration(const QString& section){
mSectionConfigurationName = section;
}
void PluginDataAPI::loadConfiguration(const PluginCapability& pCapability){
if( mSectionConfigurationName != "") {
for(int capabilityIndex = (pCapability == PluginCapability::ALL?PluginCapability::CONTACTS:pCapability); capabilityIndex != (pCapability == PluginCapability::ALL?PluginCapability::LAST:pCapability+1) ; ++capabilityIndex){
PluginCapability currentCapability = static_cast<PluginCapability>(capabilityIndex);
std::shared_ptr<linphone::Config> config = static_cast<linphone::Core*>(mLinphoneCore)->getConfig();
QVariantMap importData;
std::string sectionName = (mSectionConfigurationName+"_"+QString::number(capabilityIndex)).toStdString();
std::list<std::string> keys = config->getKeysNamesList(sectionName);
for(auto key : keys){
std::string value = config->getString(sectionName, key, "");
importData[QString::fromLocal8Bit(key.c_str(), int(key.size()))] = QString::fromLocal8Bit(value.c_str(), int(value.size()));
}
//Do not use setInputFields(importData); as we don't want to save the configuration
mInputFields[currentCapability] = importData;
emit inputFieldsChanged(currentCapability, mInputFields[currentCapability]);
}
}
}
void PluginDataAPI::saveConfiguration(const PluginCapability& pCapability){
if( mSectionConfigurationName != "") {
auto inputs = getInputFieldsToSave(pCapability);
for(QMap<PluginCapability, QVariantMap>::Iterator input = inputs.begin() ; input != inputs.end() ; ++input){
PluginCapability currentCapability = input.key();
std::string sectionName = (mSectionConfigurationName+"_"+QString::number(currentCapability)).toStdString();
std::shared_ptr<linphone::Config> config = static_cast<linphone::Core*>(mLinphoneCore)->getConfig();
QVariantMap inputsToSave = inputs[currentCapability];
config->cleanSection(sectionName);// Remove fields that doesn't exist anymore (like temporary variables)
for(auto field = inputsToSave.begin() ; field != inputsToSave.end() ; ++field)
config->setString(sectionName, qPrintable(field.key()), qPrintable(field.value().toString()));
}
}
}
void PluginDataAPI::cleanAllConfigurations(){
for(int capabilityIndex = PluginCapability::ALL ; capabilityIndex != PluginCapability::LAST ; ++capabilityIndex){
std::string sectionName = (mSectionConfigurationName+"_"+QString::number(capabilityIndex)).toStdString();
std::shared_ptr<linphone::Config> config = static_cast<linphone::Core*>(mLinphoneCore)->getConfig();
config->cleanSection(sectionName);
}
}
//----------------------------- -------------------------------------------------------
QPluginLoader * PluginDataAPI::getPluginLoader(){
return mPluginLoader;
}

View file

@ -0,0 +1,55 @@
#include "include/LinphoneApp/PluginNetworkHelper.hpp"
#include <QObject>
#include <QtNetwork>
// This class is used to define network operation to retrieve Addresses from Network
PluginNetworkHelper::PluginNetworkHelper(){}
PluginNetworkHelper::~PluginNetworkHelper(){}
void PluginNetworkHelper::request(){ // Create QNetworkReply and make network requests
QNetworkRequest request(prepareRequest());
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
mNetworkReply = mManager.get(request);
#if QT_CONFIG(ssl)
mNetworkReply->ignoreSslErrors();
#endif
QNetworkReply *data = mNetworkReply.data();
QObject::connect(data, &QNetworkReply::readyRead, this, &PluginNetworkHelper::handleReadyData);
QObject::connect(data, &QNetworkReply::finished, this, &PluginNetworkHelper::handleFinished);
QObject::connect(data, QNonConstOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &PluginNetworkHelper::handleError);
#if QT_CONFIG(ssl)
QObject::connect(data, &QNetworkReply::sslErrors, this, &PluginNetworkHelper::handleSslErrors);
#endif
}
void PluginNetworkHelper::handleReadyData(){
mBuffer.append(mNetworkReply->readAll());
}
void PluginNetworkHelper::handleFinished (){
if (mNetworkReply->error() == QNetworkReply::NoError){
mBuffer.append(mNetworkReply->readAll());
emit requestFinished(mBuffer);
}else {
qWarning() << mNetworkReply->errorString();
emit message(QtWarningMsg, "Error while dealing with network. See logs for details.");
}
mBuffer.clear();
}
void PluginNetworkHelper::handleError (QNetworkReply::NetworkError code) {
if (code != QNetworkReply::OperationCanceledError) {
QString url = mNetworkReply->url().host();
QString errorString = mNetworkReply->errorString();
qWarning() << QStringLiteral("Download failed: %1 from %2").arg(errorString).arg(url);
}
}
void PluginNetworkHelper::handleSslErrors (const QList<QSslError> &sslErrors){
#if QT_CONFIG(ssl)
for (const QSslError &error : sslErrors)
qWarning() << QStringLiteral("SSL error %1 : %2").arg(error.error()).arg(error.errorString());
#else
Q_UNUSED(sslErrors);
#endif
}

View file

@ -0,0 +1,271 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PluginsManager.hpp"
//#include "ContactsImporterModel.hpp"
#include "include/LinphoneApp/LinphonePlugin.hpp"
#include "include/LinphoneApp/PluginNetworkHelper.hpp"
#include "utils/Utils.hpp"
#include "app/paths/Paths.hpp"
#include "components/contact/VcardModel.hpp"
#include "components/contacts/ContactsListModel.hpp"
#include "components/contacts/ContactsImporterListModel.hpp"
#include "components/contacts/ContactsImporterModel.hpp"
#include "components/core/CoreManager.hpp"
#include "components/sip-addresses/SipAddressesModel.hpp"
#include <QDir>
#include <QPluginLoader>
#include <QDebug>
#include <QJsonDocument>
#include <QFileDialog>
#include <QMessageBox>
// =============================================================================
QMap<QString, QString> PluginsManager::gPluginsMap;
QString PluginsManager::gPluginsConfigSection = "AppPlugin";
PluginsManager::PluginsManager(QObject * parent) : QObject(parent){
}
QPluginLoader * PluginsManager::getPlugin(const QString &pluginIdentity){
QStringList pluginPaths = Paths::getPluginsAppFolders();// Get all paths
if( gPluginsMap.contains(pluginIdentity)){
for(int i = 0 ; i < pluginPaths.size() ; ++i) {
QString pluginPath = pluginPaths[i] +gPluginsMap[pluginIdentity];
QPluginLoader * loader = new QPluginLoader(pluginPath);
loader->setLoadHints(0); // this force Qt to unload the plugin from memory when we request it. Be carefull by not having a plugin instance or data created inside the plugin after the unload.
if( auto instance = loader->instance()) {
auto plugin = qobject_cast< LinphonePlugin* >(instance);
if (plugin )
return loader;
else{
qWarning() << loader->errorString();
loader->unload();
}
}
delete loader;
}
}
return nullptr;
}
void * PluginsManager::createInstance(const QString &pluginIdentity){
void * dataInstance = nullptr;
LinphonePlugin * plugin = nullptr;
if( gPluginsMap.contains(pluginIdentity)){
QStringList pluginPaths = Paths::getPluginsAppFolders();
for(int i = 0 ; i < pluginPaths.size() ; ++i) {
QString pluginPath = pluginPaths[i] +gPluginsMap[pluginIdentity];
QPluginLoader * loader = new QPluginLoader(pluginPath);
loader->setLoadHints(0); // this force Qt to unload the plugin from memory when we request it. Be carefull by not having a plugin instance or data created inside the plugin after the unload.
if( auto instance = loader->instance()) {
plugin = qobject_cast< LinphonePlugin* >(instance);
if (plugin) {
try{
dataInstance = plugin->createInstance(CoreManager::getInstance()->getCore().get(), loader);
return dataInstance;
}catch(...){
loader->unload();
}
}else
loader->unload();
}
delete loader;
}
}
return dataInstance;
}
QJsonDocument PluginsManager::getJson(const QString &pluginIdentity){
QJsonDocument doc;
QPluginLoader * pluginLoader = getPlugin(pluginIdentity);
if( pluginLoader ){
auto instance = pluginLoader->instance();
if( instance ){
LinphonePlugin * plugin = qobject_cast< LinphonePlugin* >(instance);
if( plugin ){
doc = QJsonDocument::fromJson(plugin->getGUIDescriptionToJson().toUtf8());
}
}
pluginLoader->unload();
delete pluginLoader;
}
return doc;
}
QList<PluginsModel*> PluginsManager::getImporterModels(const QStringList &capabilities){
QList<PluginsModel*> models;
for(int i = 0 ; i < capabilities.size() ; ++i){
if( capabilities[i] == "CONTACTS")
models += CoreManager::getInstance()->getContactsImporterListModel()->getList();
}
return models;
}
void PluginsManager::openNewPlugin(const QString &pTitle){
QString fileName = QFileDialog::getOpenFileName(nullptr, pTitle);
QString pluginIdentity;
QStringList capabilities;
QList<PluginsModel*> modelsToReset;
int doCopy = QMessageBox::Yes;
bool cannotRemovePlugin = false;
//QVersionNumber pluginVersion, apiVersion = LinphonePlugin::gPluginVersion;
if(fileName != ""){
QFileInfo fileInfo(fileName);
QString path = Utils::coreStringToAppString(Paths::getPluginsAppDirPath());
if( !QLibrary::isLibrary(fileName)){
QMessageBox::information(nullptr, pTitle, "The file is not a plugin");
}else{
QPluginLoader loader(fileName);
loader.setLoadHints(0);
QJsonObject metaData = loader.metaData()["MetaData"].toObject();
if( metaData.contains("ID") && metaData.contains("Capabilities")){
capabilities = metaData["Capabilities"].toString().toUpper().remove(' ').split(",");
pluginIdentity = metaData["ID"].toString();
}
if(!pluginIdentity.isEmpty()){// Check all plugins that have this title
QStringList oldPlugins;
if( gPluginsMap.contains(pluginIdentity))
oldPlugins << gPluginsMap[pluginIdentity];
if( QFile::exists(path+fileInfo.fileName()))
oldPlugins << path+fileInfo.fileName();
if(oldPlugins.size() > 0){
doCopy = QMessageBox::question(nullptr, pTitle, "The plugin already exists. Do you want to overwrite it?\n"+oldPlugins.join('\n'), QMessageBox::Yes, QMessageBox::No);
if( doCopy == QMessageBox::Yes){
if(gPluginsMap.contains(pluginIdentity)){
auto importers = CoreManager::getInstance()->getContactsImporterListModel()->getList();
for(auto importer : importers){
QJsonObject pluginMetaData(importer->getDataAPI()->getPluginLoader()->metaData());
if( pluginMetaData.contains("ID") && pluginMetaData["ID"].toString() == pluginIdentity){
importer->setDataAPI(nullptr);
modelsToReset.append(importer);
}
}
QStringList pluginPaths = Paths::getPluginsAppFolders();
for(int i = 0 ; !cannotRemovePlugin && i < pluginPaths.size()-1 ; ++i) {// Ignore the last path as it is the app folder
QString pluginPath = pluginPaths[i];
if(QFile::exists(pluginPath+gPluginsMap[pluginIdentity])){
cannotRemovePlugin = !QFile::remove(pluginPath+gPluginsMap[pluginIdentity]);
}
}
}
if(!cannotRemovePlugin && QFile::exists(path+fileInfo.fileName()))
cannotRemovePlugin = !QFile::remove(path+fileInfo.fileName());
if(!cannotRemovePlugin)
gPluginsMap[pluginIdentity] = "";
}
}
}else
doCopy = QMessageBox::No;
if(doCopy == QMessageBox::Yes ){
if( cannotRemovePlugin)// Qt will not unload library from memory so files cannot be removed. See https://bugreports.qt.io/browse/QTBUG-68880
QMessageBox::information(nullptr, pTitle, "The plugin cannot be replaced. You have to exit the application and delete manually the plugin file in\n"+path);
else if( !QFile::copy(fileName, path+fileInfo.fileName()))
QMessageBox::information(nullptr, pTitle, "The plugin cannot be copied. You have to copy manually the plugin file to\n"+path);
else {
gPluginsMap[pluginIdentity] = fileInfo.fileName();
for(auto importer : modelsToReset)
importer->setDataAPI(static_cast<PluginDataAPI*>(createInstance(pluginIdentity)));
}
}
}
}
}
QVariantList PluginsManager::getPlugins(const int& capabilities) {
QVariantList plugins;
QStringList pluginPaths = Paths::getPluginsAppFolders();
if(capabilities<0)
gPluginsMap.clear();
for(int pathIndex = pluginPaths.size()-1 ; pathIndex >= 0 ; --pathIndex) {// Start from app package. This sort ensure the priority on user plugins
QString pluginPath = pluginPaths[pathIndex];
QDir dir(pluginPath);
QStringList pluginFiles = dir.entryList(QDir::Files);
for(int i = 0 ; i < pluginFiles.size() ; ++i) {
if( QLibrary::isLibrary(pluginPath+pluginFiles[i])){
QPluginLoader loader(pluginPath+pluginFiles[i]);
loader.setLoadHints(0); // this force Qt to unload the plugin from memory when we request it. Be carefull by not having a plugin instance or data created inside the plugin after the unload.
if (auto instance = loader.instance()) {
LinphonePlugin * plugin = qobject_cast< LinphonePlugin* >(instance);
if ( plugin){
QJsonObject metaData = loader.metaData()["MetaData"].toObject();
if( metaData.contains("ID")){
bool getIt = false;
if(capabilities>=0 ){
if(metaData.contains("Capabilities")){
QString pluginCapabilities = metaData["Capabilities"].toString().toUpper().remove(' ');
if( (capabilities & PluginDataAPI::CONTACTS) == PluginDataAPI::CONTACTS && pluginCapabilities.contains("CONTACTS")){
getIt = true;
}
}else
qWarning()<< "The plugin " << pluginFiles[i] << " must have Capabilities in its metadata";
}else
getIt = true;
if(getIt){
QJsonDocument doc = QJsonDocument::fromJson(plugin->getGUIDescriptionToJson().toUtf8());
QVariantMap desc;
desc["pluginTitle"] = doc["pluginTitle"];
desc["pluginID"] = metaData["ID"].toString();
if(!doc["pluginTitle"].toString().isEmpty()){
gPluginsMap[metaData["ID"].toString()] = pluginFiles[i];
plugins.push_back(desc);
}
}
}else
qWarning()<< "The plugin " << pluginFiles[i] << " must have ID in its metadata";
} else {
qWarning()<< "The plugin " << pluginFiles[i] << " should be updated to this version of API : " << loader.metaData()["IID"].toString();
}
loader.unload();
} else {
qWarning()<< "The plugin " << pluginFiles[i] << " cannot be used : " << loader.errorString();
}
}
}
std::sort(plugins.begin(), plugins.end());
}
return plugins;
}
QVariantMap PluginsManager::getPluginDescription(const QString& pluginIdentity) {
QVariantMap description;
QJsonDocument doc = getJson(pluginIdentity);
description = doc.toVariant().toMap();
return description;
}
QVariantMap PluginsManager::getDefaultValues(const QString& pluginIdentity){
QVariantMap defaultValues;
QVariantMap description;
QJsonDocument doc = getJson(pluginIdentity);
description = doc.toVariant().toMap();
for(auto field : description["fields"].toList()){
auto details = field.toMap();
if( details.contains("fieldId") && details.contains("defaultData")){
defaultValues[details["fieldId"].toString()] = details["defaultData"].toString();
}
}
return defaultValues;
}

View file

@ -0,0 +1,47 @@
//const QVersionNumber ContactsImporterPlugin::gPluginVersion = QVersionNumber::fromString(PLUGIN_CONTACT_VERSION);
//const QVersionNumber ContactsImporterPlugin::gPluginVersion = QVersionNumber::fromString("1.0.0");
//const QVersionNumber _ContactsImporterPlugin::gPluginVersion = QVersionNumber::fromString("1.0.0");
#ifndef PLUGINS_MANAGER_MODEL_H_
#define PLUGINS_MANAGER_MODEL_H_
#include <QObject>
#include <QVariantList>
// =============================================================================
class ContactsImporterModel;
class PluginDataAPI;
class QPluginLoader;
class PluginsModel : public QObject{
public:
PluginsModel(QObject *parent = nullptr) : QObject(parent){}
virtual ~PluginsModel(){}
virtual void setDataAPI(PluginDataAPI*) = 0;
virtual PluginDataAPI* getDataAPI() = 0;
virtual int getIdentity()const = 0;
virtual QVariantMap getFields() = 0;
};
class PluginsManager : public QObject{
Q_OBJECT
public:
PluginsManager (QObject *parent = Q_NULLPTR);
static QPluginLoader * getPlugin(const QString &pluginIdentity); // Return a plugin loader with Hints to 0 (unload will force Qt to remove the plugin from memory).
static QVariantList getPlugins(const int& capabilities = -1); // Return all loaded plugins that have selected capabilities (PluginCapability flags)
static void * createInstance(const QString &pluginIdentity); //Return a data instance from a plugin name.
static QJsonDocument getJson(const QString &pluginIdentity); // Get the description of the plugin int the Json format.
Q_INVOKABLE static void openNewPlugin(const QString &pTitle); // Open a File Dialog. Test if the file can be load and have a matched version. Replace old plugins from custom paths and with the same plugin title.
static QVariantMap getDefaultValues(const QString& pluginIdentity); // Get the default values of each fields for th eplugin
QVariantMap getPluginDescription(const QString& pluginIdentity);
QList<PluginsModel*> getImporterModels(const QStringList &capabilities);
static QMap<QString, QString> gPluginsMap; // Map between Identity and plugin path
static QString gPluginsConfigSection; // The root name of the plugin's section in configuration file
};
#endif // CONTACTS_IMPORTER_PLUGINS_MANAGER_MODEL_H_

View file

@ -6,24 +6,37 @@ import Common.Styles 1.0
Column {
id: formTable
width: parent.width
property alias titles: header.model
property bool disableLineTitle: false
property int legendLineWidth: FormTableStyle.entry.width
property var maxWidthStyle : FormTableStyle.entry.maxWidth
readonly property double maxItemWidth: {
var n = titles.length
var curWidth = (width - FormTableStyle.entry.width) / n - (n - 1) * FormTableLineStyle.spacing
var maxWidth = FormTableStyle.entry.maxWidth
return curWidth < maxWidth ? curWidth : maxWidth
}
readonly property double maxItemWidth: computeMaxItemWidth()
// ---------------------------------------------------------------------------
function updateMaxItemWidth(){
maxItemWidth = computeMaxItemWidth();
}
function computeMaxItemWidth(){
var n = 1;
// if( titles)
// n = titles.length
// else{
for(var line = 0 ; line < formTable.visibleChildren.length ; ++line){
var column = formTable.visibleChildren[line].visibleChildren.length;
n = Math.max(n, column-1);
}
// }
var curWidth = (width - (disableLineTitle?0:legendLineWidth) ) /n - FormTableLineStyle.spacing
var maxWidth = maxWidthStyle
return curWidth < maxWidth ? curWidth : maxWidth
}
spacing: FormTableStyle.spacing
width: parent.width
// ---------------------------------------------------------------------------

View file

@ -100,5 +100,6 @@ Switch {
anchors.fill: parent
onClicked: control.enabled && control.clicked()
onPressed: control.enabled && control.forceActiveFocus()
}
}

View file

@ -1,120 +1,319 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.5
import Common 1.0
import Linphone 1.0
import App.Styles 1.0
import Linphone.Styles 1.0
import Common.Styles 1.0
import ContactsImporterPluginsManager 1.0
import 'SettingsAdvanced.js' as Logic
// =============================================================================
TabContainer {
Column {
spacing: SettingsWindowStyle.forms.spacing
width: parent.width
// -------------------------------------------------------------------------
// Logs.
// -------------------------------------------------------------------------
Form {
title: qsTr('logsTitle')
width: parent.width
FormLine {
FormGroup {
label: qsTr('logsFolderLabel')
FileChooserButton {
selectedFile: SettingsModel.logsFolder
selectFolder: true
onAccepted: SettingsModel.logsFolder = selectedFile
}
Column {
spacing: SettingsWindowStyle.forms.spacing
width: parent.width
// -------------------------------------------------------------------------
// Logs.
// -------------------------------------------------------------------------
Form {
title: qsTr('logsTitle')
width: parent.width
FormLine {
FormGroup {
label: qsTr('logsFolderLabel')
FileChooserButton {
selectedFile: SettingsModel.logsFolder
selectFolder: true
onAccepted: SettingsModel.logsFolder = selectedFile
}
}
}
FormLine {
FormGroup {
label: qsTr('logsUploadUrlLabel')
TextField {
readOnly: true
text: SettingsModel.logsUploadUrl
onEditingFinished: SettingsModel.logsUploadUrl = text
}
}
}
FormLine {
FormGroup {
label: qsTr('logsEnabledLabel')
Switch {
checked: SettingsModel.logsEnabled
onClicked: SettingsModel.logsEnabled = !checked
}
}
}
}
}
FormLine {
FormGroup {
label: qsTr('logsUploadUrlLabel')
TextField {
readOnly: true
text: SettingsModel.logsUploadUrl
onEditingFinished: SettingsModel.logsUploadUrl = text
}
Row {
anchors.right: parent.right
spacing: SettingsAdvancedStyle.buttons.spacing
TextButtonB {
text: qsTr('cleanLogs')
onClicked: Logic.cleanLogs()
}
TextButtonB {
enabled: !sendLogsBlock.loading && SettingsModel.logsEnabled
text: qsTr('sendLogs')
onClicked: sendLogsBlock.execute()
}
}
}
FormLine {
FormGroup {
label: qsTr('logsEnabledLabel')
Switch {
checked: SettingsModel.logsEnabled
onClicked: SettingsModel.logsEnabled = !checked
}
RequestBlock {
id: sendLogsBlock
action: CoreManager.sendLogs
width: parent.width
Connections {
target: CoreManager
onLogsUploaded: Logic.handleLogsUploaded(url)
}
}
}
FormEmptyLine {}
}
Row {
anchors.right: parent.right
spacing: SettingsAdvancedStyle.buttons.spacing
TextButtonB {
text: qsTr('cleanLogs')
onClicked: Logic.cleanLogs()
}
TextButtonB {
enabled: !sendLogsBlock.loading && SettingsModel.logsEnabled
text: qsTr('sendLogs')
onClicked: sendLogsBlock.execute()
}
}
RequestBlock {
id: sendLogsBlock
action: CoreManager.sendLogs
width: parent.width
Connections {
target: CoreManager
onLogsUploaded: Logic.handleLogsUploaded(url)
}
}
onVisibleChanged: sendLogsBlock.setText('')
// -------------------------------------------------------------------------
// Developer settings.
// -------------------------------------------------------------------------
Form {
title: qsTr('developerSettingsTitle')
visible: SettingsModel.developerSettingsEnabled
width: parent.width
FormLine {
FormGroup {
label: qsTr('developerSettingsEnabledLabel')
Switch {
checked: SettingsModel.developerSettingsEnabled
onClicked: SettingsModel.developerSettingsEnabled = !checked
}
onVisibleChanged: sendLogsBlock.setText('')
// -------------------------------------------------------------------------
// ADDRESS BOOK
// -------------------------------------------------------------------------
Form {
title: qsTr('contactsTitle')
width: parent.width
FormTable {
id:contactsImporterTable
width :parent.width
legendLineWidth:0
disableLineTitle:importerRepeater.count!=1
titles: (importerRepeater.count==1? getTitles(): [])
function getTitles(repeaterModel){
var fields = importerRepeater.itemAt(0).pluginDescription['fields'];
var t = [''];
for(var i = 0 ; i < fields.length ; ++i){
t.push(fields[i]['placeholder']);
}
return t;
}
Repeater{
id:importerRepeater
model:ContactsImporterListProxyModel{id:contactsImporterList}
delegate : FormTable{
//readonly property double maxItemWidth: contactsImporterTable.maxItemWidth
property var pluginDescription : importerLine.pluginDescription
width :parent.width
legendLineWidth:80
disableLineTitle:true
FormTableLine {
id:importerLine
property var fields : modelData.fields
property int identity : modelData.identity
property var pluginDescription : ContactsImporterPluginsManager.getContactsImporterPluginDescription(fields["pluginID"]) // Plugin definition
FormTableEntry {
Row{
width :parent.width
ActionButton {
id:removeImporter
icon: 'cancel'
iconSize:CallsStyle.entry.iconActionSize
onClicked:ContactsImporterListModel.removeContactsImporter(modelData)
}
Text{
height:parent.height
width:parent.width-removeImporter.width
text:importerLine.pluginDescription['pluginTitle']
horizontalAlignment:Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
wrapMode: Text.WordWrap
font {
bold: true
pointSize: FormTableStyle.entry.text.pointSize
}
TooltipArea{
text:importerLine.pluginDescription['pluginDescription']
}
}
}
}
Repeater{
model:importerLine.pluginDescription['fields']
delegate: FormTableEntry {
Loader{
sourceComponent: (modelData['type']==0 ? textComponent:textFieldComponent)
active:true
width:parent.width
Component{
id: textComponent
Text {
id: text
color: FormTableStyle.entry.text.color
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: importerLine.fields[modelData['fieldId']]?importerLine.fields[modelData['fieldId']]:''
height: FormTableStyle.entry.height
width: parent.width
font {
bold: true
pointSize: FormTableStyle.entry.text.pointSize
}
}
}
Component{
id: textFieldComponent
TextField {
readOnly: false
width:parent.width
placeholderText : modelData['placeholder']
text: importerLine.fields[modelData['fieldId']]?importerLine.fields[modelData['fieldId']]:''
echoMode: (modelData['hiddenText']?TextInput.Password:TextInput.Normal)
onEditingFinished:{
importerLine.fields[modelData['fieldId']] = text
}
Component.onCompleted: importerLine.fields[modelData['fieldId']] = text
}
}
}
}
}// Repeater : Fields
FormTableEntry {
Switch {
checked: modelData.fields["enabled"]>0
onClicked: {
checked = !checked
importerLine.fields["enabled"] = (checked?1:0)
ContactsImporterListModel.addContactsImporter(importerLine.fields, importerLine.identity)
if(checked){
ContactsImporterListModel.importContacts(importerLine.identity)
}else
contactsImporterStatus.text = ''
}
}
}//FormTableEntry
}//FormTableLine
FormTableLine{
width:parent.width-parent.legendLineWidth
FormTableEntry {
id:contactsImporterStatusEntry
visible:contactsImporterStatus.text!==''
width:parent.width
TextEdit{
id:contactsImporterStatus
property bool isError:false
selectByMouse: true
readOnly:true
color: (isError?SettingsAdvancedStyle.error.color:SettingsAdvancedStyle.info.color)
width:parent.width
horizontalAlignment:Text.AlignRight
font {
italic: true
pointSize: SettingsAdvancedStyle.info.pointSize
}
Connections{
target:modelData
onStatusMessage:{contactsImporterStatus.isError=false;contactsImporterStatus.text=message;}
onErrorMessage:{contactsImporterStatus.isError=true;contactsImporterStatus.text=message;}
}
}
}
}
}//Column
}// Repeater : Importer
}
Row{
spacing:SettingsAdvancedStyle.buttons.spacing
anchors.horizontalCenter: parent.horizontalCenter
ActionButton {
icon: 'options'
iconSize:CallsStyle.entry.iconActionSize
onClicked:{
ContactsImporterPluginsManager.openNewPlugin();
pluginChoice.model = ContactsImporterPluginsManager.getPlugins();
}
}
ComboBox{
id: pluginChoice
model:ContactsImporterPluginsManager.getPlugins()
textRole: "pluginTitle"
displayText: currentIndex === -1 ? 'No Plugins to load' : currentText
Text{// Hack, combobox show empty text when empty
anchors.fill:parent
visible:pluginChoice.currentIndex===-1
verticalAlignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
text: 'No Plugins to load'
font {
bold:false
italic: true
pointSize: FormTableStyle.entry.text.pointSize
}
}
Connections{
target:SettingsModel
onContactImporterChanged:pluginChoice.model=ContactsImporterPluginsManager.getPlugins()
}
}
ActionButton {
icon: 'add'
iconSize:CallsStyle.entry.iconActionSize
visible:pluginChoice.currentIndex>=0
onClicked:{
if( pluginChoice.currentIndex >= 0)
ContactsImporterListModel.createContactsImporter({"pluginID":pluginChoice.model[pluginChoice.currentIndex]["pluginID"]})
}
}
}
}
// -------------------------------------------------------------------------
// Developer settings.
// -------------------------------------------------------------------------
Form {
title: qsTr('developerSettingsTitle')
visible: SettingsModel.developerSettingsEnabled
width: parent.width
FormLine {
FormGroup {
label: qsTr('developerSettingsEnabledLabel')
Switch {
checked: SettingsModel.developerSettingsEnabled
onClicked: SettingsModel.developerSettingsEnabled = !checked
}
}
}
}
}
}
}
}

View file

@ -1,10 +1,20 @@
pragma Singleton
import QtQml 2.2
import Colors 1.0
import Units 1.0
// =============================================================================
QtObject {
property QtObject buttons: QtObject {
property int spacing: 10
}
property QtObject error: QtObject {
property color color: Colors.error
}
property QtObject info: QtObject {
property color color: Colors.j
property int pointSize: Units.dp * 11
}
}

@ -1 +1 @@
Subproject commit 47a49990b579ecbd5b01c81e6e8e2f00cd662a89
Subproject commit 4a3c4b2cb39181eb2e3eaaed1617b2aa866c8d33

42
plugins/CMakeLists.txt Normal file
View file

@ -0,0 +1,42 @@
################################################################################
#
# Copyright (c) 2017-2020 Belledonne Communications SARL.
#
# This file is part of linphone-desktop
# (see https://www.linphone.org).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
cmake_minimum_required(VERSION 3.1)
## Add custom plugins
macro(get_all_subdirs result curdir)
file(GLOB children RELATIVE ${curdir} ${curdir}/*)
set(dirlist "")
foreach(child ${children})
if(IS_DIRECTORY ${curdir}/${child} AND (ENABLE_BUILD_EXAMPLES OR NOT ${child} MATCHES "example"))
list(APPEND dirlist ${child})
endif()
endforeach()
set(${result} ${dirlist})
endmacro()
get_all_subdirs(SUBDIRS ${CMAKE_CURRENT_SOURCE_DIR})
set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH};${CMAKE_INSTALL_PREFIX}/include")
foreach(subdir ${SUBDIRS})
message("Adding ${subdir} plugin")
add_subdirectory(${subdir})
endforeach()

View file

@ -0,0 +1,133 @@
################################################################################
#
# Copyright (c) 2017-2020 Belledonne Communications SARL.
#
# This file is part of linphone-desktop
# (see https://www.linphone.org).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
cmake_minimum_required(VERSION 3.1)
#-------------------------------------------------
# Customizable data
set(SOURCES
src/Plugin.cpp
src/NetworkAPI.cpp
src/DataAPI.cpp
)
set(HEADERS
src/Plugin.hpp
src/NetworkAPI.hpp
src/DataAPI.hpp
)
list(APPEND SOURCES
src/PluginMetaData.json)
set(TARGET_NAME linphonePluginExample )
#-------------------------------------------------
find_package(bctoolbox CONFIG)
set(FULL_VERSION )
bc_compute_full_version(FULL_VERSION)
set(version_major )
set(version_minor )
set(version_patch )
set(identifiers )
set(metadata )
bc_parse_full_version("${FULL_VERSION}" version_major version_minor version_patch identifiers metadata)
set(PLUGIN_VERSION "${version_major}.${version_minor}.${version_patch}")
project(${TARGET_NAME} VERSION ${PLUGIN_VERSION})
include(GNUInstallDirs)
include(CheckCXXCompilerFlag)
message("${TARGET_NAME} version : ${PLUGIN_VERSION}")
set(CMAKE_CXX_STANDARD 11)
SET_PROPERTY(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS true)
if(UNIX AND NOT APPLE)
set(CMAKE_INSTALL_RPATH "$ORIGIN;$ORIGIN/lib64;$ORIGIN/../lib64;$ORIGIN/lib;$ORIGIN/../lib")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
endif()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
list(APPEND CMAKE_PREFIX_PATH ${CMAKE_INSTALL_PREFIX}/include)
if(WIN32)
set(EXECUTABLE_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTABLE_OUTPUT_DIR} )
set( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${EXECUTABLE_OUTPUT_DIR} )
set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${EXECUTABLE_OUTPUT_DIR} )
foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} )# Apply to all configurations
string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG )
set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${EXECUTABLE_OUTPUT_DIR} )
set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${EXECUTABLE_OUTPUT_DIR} )
set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${EXECUTABLE_OUTPUT_DIR} )
endforeach( OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES )
endif()
# Build configuration
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG -DQT_NO_DEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -DQT_QML_DEBUG -DQT_DECLARATIVE_DEBUG")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG -DQT_QML_DEBUG -DQT_DECLARATIVE_DEBUG" )
if( WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_WINSOCKAPI_")#remove error from windows headers order
endif()
set(CMAKE_INCLUDE_CURRENT_DIR ON)#useful for config.h
set(QT5_PACKAGES Core Widgets Network)
set(CMAKE_AUTOMOC ON)
find_package(Qt5 COMPONENTS ${QT5_PACKAGES} REQUIRED)
find_package(Qt5 COMPONENTS ${QT5_PACKAGES_OPTIONAL} QUIET)
find_package(LinphoneCxx CONFIG)
find_package(bctoolbox CONFIG)
# ------------------------------------------------------------------------------
# Build.
# ------------------------------------------------------------------------------
add_library(${TARGET_NAME} SHARED ${SOURCES} ${HEADERS})
target_link_libraries(${TARGET_NAME} PRIVATE Qt5::Widgets Qt5::Network ${LINPHONECXX_LIBRARIES})
target_compile_options(${TARGET_NAME} PRIVATE ${COMPILE_OPTIONS})
set_source_files_properties( ${TARGET_NAME} PROPERTIES EXTERNAL_OBJECT true GENERATED true )
set_property(TARGET ${TARGET_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) #Need by Qt
target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_PREFIX_PATH} ${LINPHONECXX_INCLUDE_DIRS})
set_target_properties(${TARGET_NAME} PROPERTIES OUTPUT_NAME "${TARGET_NAME}-${PLUGIN_VERSION}")
#-------------------------------------------------------------------------------
# IDE
#-------------------------------------------------------------------------------
source_group(
"Json" REGULAR_EXPRESSION ".+\.json$"
)
#-------------------------------------------------------------------------------
# Install.
#-------------------------------------------------------------------------------
set(LINPHONE_APP_CONTACT_PLUGINS_PATH "plugins/app")
install(TARGETS ${TARGET_NAME} DESTINATION "${LINPHONE_APP_CONTACT_PLUGINS_PATH}")

View file

@ -0,0 +1,157 @@
#include "DataAPI.hpp"
#include "NetworkAPI.hpp"
#include "Plugin.hpp"
#include <QInputDialog>
#include <QPluginLoader>
#include <linphone++/proxy_config.hh>
DataAPI::DataAPI(Plugin *plugin, void * core, QPluginLoader * pluginLoader) :PluginDataAPI(plugin, core, pluginLoader){
auto proxyConfig = static_cast<linphone::Core*>(mLinphoneCore)->getDefaultProxyConfig();
QVariantMap account;
std::string domain;
if(proxyConfig)
domain = proxyConfig->getDomain();
else{
proxyConfig = static_cast<linphone::Core*>(mLinphoneCore)->createProxyConfig();
if(proxyConfig)
domain = proxyConfig->getDomain();
if(domain == "")
domain = "sip.linphone.org";
}
mInputFields[CONTACTS]["SIP_Domain"] = QString::fromLocal8Bit(domain.c_str(), int(domain.size()));
}
QString DataAPI::getUrl()const{
return mInputFields[CONTACTS]["URL"].toString();
}
QString DataAPI::getDomain()const{
return mInputFields[CONTACTS]["SIP_Domain"].toString();
}
QString DataAPI::getUsername()const{
return mInputFields[CONTACTS]["Username"].toString();
}
QString DataAPI::getPassword()const{
return mInputFields[CONTACTS]["Password"].toString();
}
QString DataAPI::getKey()const{
return mInputFields[CONTACTS]["Key"].toString();
}
bool DataAPI::isEnabled()const{
return mInputFields[CONTACTS]["enabled"].toInt()>0;
}
void DataAPI::setPassword(const QString &password){
mInputFields[CONTACTS]["Password"] = password;
}
bool DataAPI::isValid(const bool &pRequestData, QString * pError){
QStringList errors;
if( getDomain().isEmpty())
errors << "Domain is empty.";
if( getUrl().isEmpty())
errors << "Url is empty.";
if( getUsername().isEmpty())
errors << "Username is empty.";
if( getPassword().isEmpty() && getKey().isEmpty()){
if(pRequestData)
setPassword(QInputDialog::getText(nullptr, "Linphone example Address Book","Password",QLineEdit::EchoMode::Password));
if( getPassword().isEmpty())
errors << "Password is empty.";
}
if( errors.size() > 0){
if(pError)
*pError = "Data is invalid : " + errors.join(" ");
return false;
}else
return true;
}
QMap<PluginDataAPI::PluginCapability, QVariantMap> DataAPI::getInputFieldsToSave(const PluginCapability& capability){// Remove Password from config file
QMap<PluginCapability, QVariantMap> data = mInputFields;
data[CONTACTS].remove("Password");
return data;
}
void DataAPI::run(const PluginCapability& actionType){
if( actionType == PluginCapability::CONTACTS){
NetworkAPI * network = new NetworkAPI(this);
QObject::connect(this, &PluginDataAPI::dataReceived, network, &DataAPI::deleteLater);
network->startRequest();
}
}
//-----------------------------------------------------------------------------------------
void DataAPI::parse(const QByteArray& p_data){
QVector<QMultiMap<QString,QString> > parsedData;
QString statusText;
if(!p_data.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(p_data);
QJsonObject responses = doc.object();
QString status = responses["status"].toString();
QString comment = responses["comment"].toString();
if( responses.size() == 0){
statusText = "Contacts are not in Json format.";
}else if( status != "OK"){
statusText = status;
if( statusText.isEmpty())
statusText = "Cannot parse the request: The URL may not be valid.";
if(!comment.isEmpty())
statusText += " "+comment;
if( mInputFields[CONTACTS].contains("Key")){
QVariantMap newInputs = mInputFields[CONTACTS];
newInputs.remove("Key");// Reset key on error
setInputFields(CONTACTS, newInputs);
}
}else{
if( responses.contains("key")){
QVariantMap newInputs = mInputFields[CONTACTS];
newInputs["Key"] = responses["key"].toString();
setInputFields(CONTACTS, newInputs);
}
if( responses.contains("contacts")){
QJsonArray contacts = responses["contacts"].toArray();
int contactCount = 0;
for(int i = 0 ; i < contacts.size() ; ++i){
QMultiMap<QString, QString> cardData;
QJsonObject contact = contacts[i].toObject();
QString phoneNumber = contact["number"].toString();
QStringList name;
bool haveData = false;
QString company = contact["company"].toString();
if( contact.contains("firstname") && contact["firstname"].toString() != "")
name << contact["firstname"].toString();
if( contact.contains("surname") && contact["surname"].toString() != "")
name << contact["surname"].toString();
if(name.size() > 0){
QString username = name.join(" ");
cardData.insert("displayName", username);
}
if(!phoneNumber.isEmpty()) {
cardData.insert("phoneNumber", phoneNumber);
cardData.insert("sipUsername", phoneNumber);
haveData = true;
}
if(!company.isEmpty())
cardData.insert("organization", company);
if( haveData){
cardData.insert("sipDomain", mInputFields[CONTACTS]["SIP_Domain"].toString());
parsedData.push_back(cardData);
++contactCount;
}
}
QString messageStatus = QString::number(contactCount) +" contact"+(contactCount>1?"s":"")+" have been synchronized at "+QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
emit message(QtInfoMsg, messageStatus);
qInfo() << messageStatus;
}
}
}else
statusText = "Cannot parse the request: The URL may not be valid.";
if( !statusText.isEmpty())
emit message(QtWarningMsg, statusText);
emit dataReceived(PluginDataAPI::CONTACTS, parsedData);
}

View file

@ -0,0 +1,43 @@
#ifndef DATAAPI_HPP
#define DATAAPI_HPP
#include <QObject>
#include <QtNetwork>
#include <QVariantMap>
#include <LinphoneApp/PluginDataAPI.hpp>
#include <linphone++/core.hh>
class Plugin;
class QPluginLoader;
// Example of address book importer
class DataAPI : public PluginDataAPI
{
Q_OBJECT
public:
DataAPI(Plugin *plugin, void *core, QPluginLoader * pluginLoader);
virtual ~DataAPI(){}
QString getUrl()const;
QString getDomain()const;
QString getUsername()const;
QString getPassword()const;
QString getKey()const;
bool isEnabled()const;
void setPassword(const QString &password);
virtual bool isValid(const bool &requestData, QString * pError= nullptr);// Test data and send signal. Used to get feedback
virtual QMap<PluginDataAPI::PluginCapability, QVariantMap> getInputFieldsToSave(const PluginCapability& capability);
virtual void run(const PluginCapability& actionType);
public slots:
virtual void parse(const QByteArray& p_data);
signals:
void inputFieldsChanged(const PluginCapability& capability, const QVariantMap &inputs); // The plugin made updates on input
};
#endif // DATAAPI_HPP

View file

@ -0,0 +1,58 @@
#include "NetworkAPI.hpp"
#include "DataAPI.hpp"
#include <QInputDialog>
#include <LinphoneApp/PluginDataAPI.hpp>
NetworkAPI::NetworkAPI(DataAPI * data) : mData(data){
if(mData ) {
connect(this, SIGNAL(requestFinished(const QByteArray&)), mData, SLOT(parse(const QByteArray&)));
connect(this, &NetworkAPI::message, mData, &DataAPI::message);
}
}
NetworkAPI::~NetworkAPI(){
}
bool NetworkAPI::isEnabled()const{
return mData && mData->isEnabled();
}
bool NetworkAPI::isValid(PluginDataAPI * pData, const bool &pShowError){
QString errorMessage;
DataAPI * data = dynamic_cast<DataAPI*>(pData);
bool ok = data;
if(!ok)
errorMessage = "These data are invalid";
else
ok = pData->isValid(true, &errorMessage);
if(!ok && pShowError){
qWarning() << errorMessage;
emit message(QtMsgType::QtWarningMsg, errorMessage);
}
return ok;
}
//-----------------------------------------------------------------------------------------
QString NetworkAPI::prepareRequest()const{
QString url = mData->getUrl()+"?user="+mData->getUsername()+"&";
if( mData->getKey() != "")
url += "key="+mData->getKey();
else
url += "password="+mData->getPassword();
return url;
}
void NetworkAPI::startRequest() {
bool doRequest = false;
if(isValid(mData)){
if(isEnabled()){
mCurrentStep=0;
doRequest = true;
}
}
if(doRequest)
request();
else
mData->parse(QByteArray());
}

View file

@ -0,0 +1,33 @@
#ifndef NETWORKAPI_HPP
#define NETWORKAPI_HPP
#include <QObject>
#include <QtNetwork>
#include <LinphoneApp/PluginNetworkHelper.hpp>
class DataAPI;
class PluginDataAPI;
// Interface between Network API and Data.
class NetworkAPI : public PluginNetworkHelper
{
Q_OBJECT
public:
NetworkAPI(DataAPI * data);
virtual ~NetworkAPI();
bool isEnabled()const; // Interface to test if data is enabled
bool isValid(PluginDataAPI * pData, const bool &pShowError = true);// Test if data is valid
virtual QString prepareRequest()const;// Prepare request for URL
void startRequest();
// Data
DataAPI * mData;
int mCurrentStep;
};
#endif // NETWORKAPI_HPP

View file

@ -0,0 +1,76 @@
/******************************************************************************
*
* Copyright (c) 2017-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*******************************************************************************/
#include "Plugin.hpp"
#include <QJsonDocument>
#include <QJsonArray>
#include "DataAPI.hpp"
#include "NetworkAPI.hpp"
QString Plugin::getGUIDescriptionToJson()const{
QJsonObject description;
description["pluginTitle"] = "Plugin Example";
description["pluginDescription"] = "This is a test plugin to import an address book from an URL";
QJsonObject field;
QJsonArray fields;
field["placeholder"] = "SIP Domain";
field["fieldId"] = "SIP_Domain";
field["defaultData"] = ""; // Set by the Data instance from Core
field["type"] = 1;
field["capability"] = PluginDataAPI::CONTACTS;
fields.append(field);
field = QJsonObject();
field["placeholder"] = "URL";
field["fieldId"] = "URL";
field["defaultData"] = "";
field["type"] = 1;
field["capability"] = PluginDataAPI::CONTACTS;
fields.append(field);
field = QJsonObject();
field["placeholder"] = "Username";
field["fieldId"] = "Username";
field["defaultData"] = "username@domain.com";
field["type"] = 1;
field["capability"] = PluginDataAPI::CONTACTS;
fields.append(field);
field = QJsonObject();
field["placeholder"] = "Password";
field["fieldId"] = "Password";
field["defaultData"] = "This is a pass";
field["type"] = 1;
field["hiddenText"] = true;
field["capability"] = PluginDataAPI::CONTACTS;
fields.append(field);
description["fields"] = fields;
QJsonDocument document(description);
return document.toJson();
}
PluginDataAPI * Plugin::createInstance(void * core, QPluginLoader *pluginLoader){
return new DataAPI(this, core, pluginLoader);
}

View file

@ -0,0 +1,45 @@
/******************************************************************************
*
* Copyright (c) 2017-2020 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*******************************************************************************/
#ifndef PLUGIN_HPP
#define PLUGIN_HPP
#include <QObject>
#include <QtPlugin>
#include <LinphoneApp/LinphonePlugin.hpp>
#include <LinphoneApp/PluginDataAPI.hpp>
#include <linphone++/core.hh>
//-----------------------------
class QPluginLoader;
class Plugin : public QObject, public LinphonePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID LinphonePlugin_iid FILE "PluginMetaData.json")
Q_INTERFACES(LinphonePlugin)
public:
Plugin(){}
virtual QString getGUIDescriptionToJson() const;
virtual PluginDataAPI * createInstance(void* core, QPluginLoader * pluginLoader);
};
#endif

View file

@ -0,0 +1,6 @@
{
"ID" : "ExamplePlugin. This ID must be unique from all plugins.",
"Version" : "1.0.0",
"Capabilities" : "Contacts",
"Description" : "This is an example for describing your plugin. Replace all fields above to be usable by the Application."
}