diff --git a/Linphone/CMakeLists.txt b/Linphone/CMakeLists.txt index 19d324f91..711762bcd 100644 --- a/Linphone/CMakeLists.txt +++ b/Linphone/CMakeLists.txt @@ -228,7 +228,10 @@ foreach(T ${QT_PACKAGES}) target_link_libraries(${TARGET_NAME} PRIVATE Qt6::${T}) endforeach() - +if (WIN32) + target_link_libraries(${TARGET_NAME} PRIVATE runtimeobject) + target_link_libraries(${TARGET_NAME} PRIVATE shlwapi propsys shell32) +endif() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake" "${CMAKE_CURRENT_BINARY_DIR}/config.h") @@ -239,4 +242,4 @@ if (WIN32) install(FILES "$" DESTINATION ${CMAKE_INSTALL_BINDIR}) endforeach () endif() -endif() \ No newline at end of file +endif() diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 06a667e35..5ca546900 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -122,6 +122,10 @@ #include "core/event-count-notifier/EventCountNotifierSystemTrayIcon.hpp" #endif // if defined(Q_OS_MACOS) +#if defined(Q_OS_WIN) +#include "core/notifier/WindowsNotificationBackend.hpp" +#endif + DEFINE_ABSTRACT_OBJECT(App) #ifdef Q_OS_LINUX @@ -614,6 +618,10 @@ int App::getEventCount() const { return mEventCountNotifier ? mEventCountNotifier->getEventCount() : 0; } +NotificationBackend *App::getNotificationBackend() const { + return mNotificationBackend; +} + //----------------------------------------------------------- // Initializations //----------------------------------------------------------- @@ -744,6 +752,8 @@ void App::initCore() { mEngine->setObjectOwnership(settings.get(), QQmlEngine::CppOwnership); mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); + mNotificationBackend = new NotificationBackend(this); + auto initLists = [this] { if (mCoreStarted) { if (!mAccountList) setAccountList(AccountList::create()); @@ -1086,7 +1096,6 @@ void App::clean() { mDateUpdateTimer.stop(); #ifdef Q_OS_WIN removeNativeEventFilter(mLockEventFilter); - delete mLockEventFilter; #endif if (mEngine) { mEngine->clearComponentCache(); @@ -1698,8 +1707,9 @@ void App::setSysTrayIcon() { // #ifdef Q_OS_WIN - if (!mLockEventFilter) mLockEventFilter = new LockEventFilter(); - connect(mLockEventFilter, &LockEventFilter::sessionUnlocked, this, [this] { emit sessionUnlocked(); }); + if (!mLockEventFilter) mLockEventFilter = new LockEventFilter(this); + connect(mLockEventFilter, &LockEventFilter::sessionLockedChanged, this, + [this](bool locked) { setSessionLocked(locked); }); installNativeEventFilter(mLockEventFilter); #endif } @@ -1807,6 +1817,20 @@ void App::setScreenRatio(float ratio) { mScreenRatio = ratio; } +#ifdef Q_OS_WIN +void App::setSessionLocked(bool locked) { + if (mSessionLocked != locked) { + mSessionLocked = locked; + emit sessionLockedChanged(); + } +} + +bool App::getSessionLocked() const { + return mSessionLocked; +} + +#endif + QAction *App::createMarkAsReadAction(QQuickWindow *window) { QAction *markAllReadAction = new QAction(tr("mark_all_read_action"), window); window->connect(markAllReadAction, &QAction::triggered, this, [this] { diff --git a/Linphone/core/App.hpp b/Linphone/core/App.hpp index 0681e9028..771c29627 100644 --- a/Linphone/core/App.hpp +++ b/Linphone/core/App.hpp @@ -43,6 +43,7 @@ class Notifier; class QQuickWindow; class QSystemTrayIcon; class DefaultTranslatorCore; +class NotificationBackend; class App : public SingleApplication, public AbstractObject { Q_OBJECT @@ -197,11 +198,18 @@ public: QString getSdkVersion(); QString getQtVersion() const; + NotificationBackend *getNotificationBackend() const; + Q_INVOKABLE void checkForUpdate(bool requestedByUser = false); float getScreenRatio() const; Q_INVOKABLE void setScreenRatio(float ratio); +#ifdef Q_OS_WIN + void setSessionLocked(bool locked); + bool getSessionLocked() const; +#endif + #ifdef Q_OS_LINUX Q_INVOKABLE void exportDesktopFile(); @@ -233,7 +241,7 @@ signals: void remainingTimeBeforeOidcTimeoutChanged(); void currentAccountChanged(); #ifdef Q_OS_WIN - void sessionUnlocked(); + void sessionLockedChanged(); #endif // void executeCommand(QString command); @@ -248,6 +256,7 @@ private: Thread *mLinphoneThread = nullptr; Notifier *mNotifier = nullptr; EventCountNotifier *mEventCountNotifier = nullptr; + NotificationBackend *mNotificationBackend = nullptr; QSystemTrayIcon *mSystemTrayIcon = nullptr; QQuickWindow *mMainWindow = nullptr; QQuickWindow *mCallsWindow = nullptr; @@ -278,6 +287,8 @@ private: float mScreenRatio = 1; QTimer mOIDCRefreshTimer; int mRemainingTimeBeforeOidcTimeout = 0; - +#ifdef Q_OS_WIN + bool mSessionLocked = false; +#endif DECLARE_ABSTRACT_OBJECT }; diff --git a/Linphone/core/CMakeLists.txt b/Linphone/core/CMakeLists.txt index 39a3d54f5..c8a54fa72 100644 --- a/Linphone/core/CMakeLists.txt +++ b/Linphone/core/CMakeLists.txt @@ -47,6 +47,7 @@ list(APPEND _LINPHONEAPP_SOURCES core/logger/QtLogger.cpp core/login/LoginPage.cpp core/notifier/Notifier.cpp + core/notifier/AbstractNotificationBackend.cpp core/path/Paths.cpp core/phone-number/PhoneNumber.cpp core/phone-number/PhoneNumberList.cpp @@ -129,6 +130,12 @@ else() # Use QDBus for Linux core/singleapplication/SingleApplicationDBusPrivate.hpp core/singleapplication/SingleApplicationDBus.cpp) endif() +if(WIN32) + list(APPEND _LINPHONEAPP_SOURCES + core/notifier/NotificationActivator.cpp + core/notifier/DesktopNotificationManagerCompat.cpp + core/notifier/WindowsNotificationBackend.cpp) +endif() if(APPLE) list(APPEND _LINPHONEAPP_SOURCES core/event-count-notifier/EventCountNotifierMacOs.m) else() diff --git a/Linphone/core/call/CallCore.cpp b/Linphone/core/call/CallCore.cpp index b4544908e..6d1026646 100644 --- a/Linphone/core/call/CallCore.cpp +++ b/Linphone/core/call/CallCore.cpp @@ -123,6 +123,7 @@ CallCore::CallCore(const std::shared_ptr &call) : QObject(nullpt auto remoteAddress = call->getCallLog()->getRemoteAddress(); mRemoteAddress = Utils::coreStringToAppString(remoteAddress->asStringUriOnly()); mRemoteUsername = Utils::coreStringToAppString(remoteAddress->getUsername()); + mCallId = Utils::coreStringToAppString(call->getCallLog()->getCallId()); auto linphoneFriend = ToolModel::findFriendByAddress(remoteAddress); if (linphoneFriend) mRemoteName = Utils::coreStringToAppString( @@ -520,6 +521,10 @@ QString CallCore::getLocalAddress() const { return mLocalAddress; } +QString CallCore::getCallId() const { + return mCallId; +} + LinphoneEnums::CallStatus CallCore::getStatus() const { return mStatus; } diff --git a/Linphone/core/call/CallCore.hpp b/Linphone/core/call/CallCore.hpp index 4c448a806..7f52d65c1 100644 --- a/Linphone/core/call/CallCore.hpp +++ b/Linphone/core/call/CallCore.hpp @@ -148,6 +148,8 @@ public: QString getRemoteAddress() const; QString getLocalAddress() const; + QString getCallId() const; + LinphoneEnums::CallStatus getStatus() const; void setStatus(LinphoneEnums::CallStatus status); @@ -334,6 +336,7 @@ private: QString mRemoteUsername; QString mRemoteAddress; QString mLocalAddress; + QString mCallId; bool mTokenVerified = false; bool mIsSecured = false; bool mIsMismatch = false; diff --git a/Linphone/core/event-filter/LockEventFilter.cpp b/Linphone/core/event-filter/LockEventFilter.cpp index bbbeddcec..2f41b9ec0 100644 --- a/Linphone/core/event-filter/LockEventFilter.cpp +++ b/Linphone/core/event-filter/LockEventFilter.cpp @@ -27,12 +27,8 @@ bool LockEventFilter::nativeEventFilter(const QByteArray &eventType, void *messa #ifdef Q_OS_WIN MSG *msg = static_cast(message); if (msg->message == WM_WTSSESSION_CHANGE) { - if (msg->wParam == WTS_SESSION_LOCK) { - return true; - } else { - emit sessionUnlocked(); - return true; - } + emit sessionLockedChanged(msg->wParam == WTS_SESSION_LOCK); + return true; } #endif diff --git a/Linphone/core/event-filter/LockEventFilter.hpp b/Linphone/core/event-filter/LockEventFilter.hpp index e6d2bd4d1..76c72a228 100644 --- a/Linphone/core/event-filter/LockEventFilter.hpp +++ b/Linphone/core/event-filter/LockEventFilter.hpp @@ -40,7 +40,7 @@ public: virtual bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override; signals: - void sessionUnlocked(); + void sessionLockedChanged(bool locked); }; #endif // LOCKEVENTFILTER_H diff --git a/Linphone/core/notifier/AbstractNotificationBackend.cpp b/Linphone/core/notifier/AbstractNotificationBackend.cpp new file mode 100644 index 000000000..e9e2cf4ec --- /dev/null +++ b/Linphone/core/notifier/AbstractNotificationBackend.cpp @@ -0,0 +1,10 @@ +#include "AbstractNotificationBackend.hpp" + +DEFINE_ABSTRACT_OBJECT(AbstractNotificationBackend) + +const QHash AbstractNotificationBackend::Notifications = { + {AbstractNotificationBackend::ReceivedMessage, Notification(AbstractNotificationBackend::ReceivedMessage, 10)}, + {AbstractNotificationBackend::ReceivedCall, Notification(AbstractNotificationBackend::ReceivedCall, 30)}}; + +AbstractNotificationBackend::AbstractNotificationBackend(QObject *parent) : QObject(parent) { +} diff --git a/Linphone/core/notifier/AbstractNotificationBackend.hpp b/Linphone/core/notifier/AbstractNotificationBackend.hpp new file mode 100644 index 000000000..102f64322 --- /dev/null +++ b/Linphone/core/notifier/AbstractNotificationBackend.hpp @@ -0,0 +1,60 @@ +#ifndef ABSTRACTNOTIFICATIONBACKEND_HPP +#define ABSTRACTNOTIFICATIONBACKEND_HPP + +#include "tool/AbstractObject.hpp" +#include +#include +#include + +struct ToastButton { + QString label; + QString argument; + ToastButton(QString title, QString arg) { + label = title; + argument = arg; + } +}; + +class AbstractNotificationBackend : public QObject, public AbstractObject { + Q_OBJECT +public: + AbstractNotificationBackend(QObject *parent = Q_NULLPTR); + ~AbstractNotificationBackend() = default; + + enum NotificationType { + ReceivedMessage, + ReceivedCall + // ReceivedFileMessage, + // SnapshotWasTaken, + // RecordingCompleted + }; + struct Notification { + Notification(int type, int timeout = 0) { + this->type = NotificationType(type); + this->timeout = timeout; + } + int getTimeout() const { + return timeout; + } + + private: + int type; + int timeout; + }; + +protected: + virtual void sendNotification(const QString &title = QString(), + const QString &message = QString(), + const QList &actions = {}) = 0; + + virtual void sendNotification(NotificationType type, QVariantMap data) = 0; + static const QHash Notifications; + +signals: + void toastActivated(const QString &args); + +private: + DECLARE_ABSTRACT_OBJECT +}; + +#endif // ABSTRACTNOTIFICATIONBACKEND_HPP diff --git a/Linphone/core/notifier/DesktopNotificationManagerCompat.cpp b/Linphone/core/notifier/DesktopNotificationManagerCompat.cpp new file mode 100644 index 000000000..5984722dc --- /dev/null +++ b/Linphone/core/notifier/DesktopNotificationManagerCompat.cpp @@ -0,0 +1,376 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** +#include + +#include "DesktopNotificationManagerCompat.hpp" +#include "NotificationActivator.hpp" +#include +#include + +#include +#include + +#include +#include + +#include "core/App.hpp" + +#pragma comment(lib, "propsys.lib") + +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::UI::Notifications; +using namespace Microsoft::WRL; + +#define RETURN_IF_FAILED(hr) \ + do { \ + HRESULT _hrTemp = hr; \ + if (FAILED(_hrTemp)) { \ + return _hrTemp; \ + } \ + } while (false) + +using namespace ABI::Windows::Data::Xml::Dom; +using namespace Microsoft::WRL::Wrappers; + +namespace DesktopNotificationManagerCompat { +HRESULT RegisterComServer(GUID clsid, const wchar_t exePath[]); +HRESULT RegisterAumidInRegistry(const wchar_t *aumid); +HRESULT EnsureRegistered(); +bool IsRunningAsUwp(); + +bool s_registeredAumidAndComServer = false; +std::wstring s_aumid; +bool s_registeredActivator = false; +bool s_hasCheckedIsRunningAsUwp = false; +bool s_isRunningAsUwp = false; + +DWORD g_comCookie = 0; + +HRESULT CreateStartMenuShortcut(const wchar_t *aumid, GUID clsid) { + // Chemin de destination du raccourci + wchar_t shortcutPath[MAX_PATH]; + SHGetFolderPathW(nullptr, CSIDL_PROGRAMS, nullptr, 0, shortcutPath); + wcsncat_s(shortcutPath, L"\\" APPLICATION_NAME L".lnk", MAX_PATH); + + // Créer le IShellLink + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (FAILED(hr)) return hr; + + // Pointer vers l'exe courant + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(nullptr, exePath, MAX_PATH); + qDebug() << "EXE path for shortcut:" << QString::fromWCharArray(exePath); + shellLink->SetPath(exePath); + shellLink->SetArguments(L""); + + // Définir les propriétés AUMID + ToastActivatorCLSID + ComPtr propStore; + hr = shellLink.As(&propStore); + if (FAILED(hr)) return hr; + + PROPVARIANT pv; + + // AUMID + InitPropVariantFromString(aumid, &pv); + propStore->SetValue(PKEY_AppUserModel_ID, pv); + PropVariantClear(&pv); + + // Toast Activator CLSID + InitPropVariantFromCLSID(clsid, &pv); + propStore->SetValue(PKEY_AppUserModel_ToastActivatorCLSID, pv); + PropVariantClear(&pv); + + propStore->Commit(); + + // Sauvegarder le fichier .lnk + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (FAILED(hr)) return hr; + + return persistFile->Save(shortcutPath, TRUE); +} + +HRESULT RegisterAumidInRegistry(const wchar_t *aumid) { + std::wstring keyPath = std::wstring(L"Software\\Classes\\AppUserModelId\\") + aumid; + + HKEY key; + LONG res = ::RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, + nullptr, &key, nullptr); + + if (res != ERROR_SUCCESS) return HRESULT_FROM_WIN32(res); + + // DisplayName obligatoire pour que Windows affiche la notification + const wchar_t *displayName = aumid; + res = ::RegSetValueExW(key, L"DisplayName", 0, REG_SZ, reinterpret_cast(displayName), + static_cast((wcslen(displayName) + 1) * sizeof(wchar_t))); + + ::RegCloseKey(key); + return HRESULT_FROM_WIN32(res); +} + +HRESULT RegisterAumidAndComServer(const wchar_t *aumid, GUID clsid) { + // If running as Desktop Bridge + qDebug() << QString("CLSID : {%1-%2-%3-%4%5-%6%7%8%9%10%11}") + .arg(clsid.Data1, 8, 16, QChar('0')) + .toUpper() + .arg(clsid.Data2, 4, 16, QChar('0')) + .toUpper() + .arg(clsid.Data3, 4, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[0], 2, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[1], 2, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[2], 2, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[3], 2, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[4], 2, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[5], 2, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[6], 2, 16, QChar('0')) + .toUpper() + .arg(clsid.Data4[7], 2, 16, QChar('0')) + .toUpper(); + if (IsRunningAsUwp()) { + // Clear the AUMID since Desktop Bridge doesn't use it, and then we're done. + // Desktop Bridge apps are registered with platform through their manifest. + // Their LocalServer32 key is also registered through their manifest. + qInfo() << "clear AUMID as it is not needed"; + + s_aumid = L""; + s_registeredAumidAndComServer = true; + return S_OK; + } + + // Copy the aumid + s_aumid = std::wstring(aumid); + qDebug() << "S_AUMID:" << s_aumid; + + // Get the EXE path + wchar_t exePath[MAX_PATH]; + DWORD charWritten = ::GetModuleFileName(nullptr, exePath, ARRAYSIZE(exePath)); + RETURN_IF_FAILED(charWritten > 0 ? S_OK : HRESULT_FROM_WIN32(::GetLastError())); + + // Register the COM server + qInfo() << "Register com server and aumid"; + RETURN_IF_FAILED(RegisterComServer(clsid, exePath)); + + qInfo() << "Register aumid in registry"; + RETURN_IF_FAILED(RegisterAumidInRegistry(aumid)); + + s_registeredAumidAndComServer = true; + return S_OK; +} + +HRESULT RegisterActivator() { + // Module needs a callback registered before it can be used. + // Since we don't care about when it shuts down, we'll pass an empty lambda here. + Module::Create([] {}); + + // If a local server process only hosts the COM object then COM expects + // the COM server host to shutdown when the references drop to zero. + // Since the user might still be using the program after activating the notification, + // we don't want to shutdown immediately. Incrementing the object count tells COM that + // we aren't done yet. + Module::GetModule().IncrementObjectCount(); + + // HRESULT hr = CoRegisterClassObject(__uuidof(NotificationActivator), factory, CLSCTX_LOCAL_SERVER, + // REGCLS_MULTIPLEUSE, &g_comCookie); + + // qInfo() << "CoRegisterClassObject result:" << Qt::hex << hr << "Cookie:" << g_comCookie; + + // factory->Release(); + + auto hr = Module::GetModule().RegisterObjects(); + qInfo() << "RegisterObjects result:" << Qt::hex << hr; + + if (FAILED(hr)) { + qWarning() << "CoRegisterClassObject ÉCHOUÉ ? Activate() jamais appelé !"; + return hr; + } + + s_registeredActivator = true; + return S_OK; +} + +HRESULT RegisterComServer(GUID clsid, const wchar_t exePath[]) { + // Turn the GUID into a string + OLECHAR *clsidOlechar; + StringFromCLSID(clsid, &clsidOlechar); + std::wstring clsidStr(clsidOlechar); + ::CoTaskMemFree(clsidOlechar); + + // Create the subkey + // Something like SOFTWARE\Classes\CLSID\{23A5B06E-20BB-4E7E-A0AC-6982ED6A6041}\LocalServer32 + std::wstring subKey = LR"(SOFTWARE\Classes\CLSID\)" + clsidStr + LR"(\LocalServer32)"; + + // Include -ToastActivated launch args on the exe + std::wstring exePathStr(exePath); + exePathStr = L"\"" + exePathStr + L"\" " + TOAST_ACTIVATED_LAUNCH_ARG; + + // We don't need to worry about overflow here as ::GetModuleFileName won't + // return anything bigger than the max file system path (much fewer than max of DWORD). + DWORD dataSize = static_cast((exePathStr.length() + 1) * sizeof(WCHAR)); + + // Register the EXE for the COM server + return HRESULT_FROM_WIN32(::RegSetKeyValue(HKEY_CURRENT_USER, subKey.c_str(), nullptr, REG_SZ, + reinterpret_cast(exePathStr.c_str()), dataSize)); +} + +HRESULT CreateToastNotifier(IToastNotifier **notifier) { + RETURN_IF_FAILED(EnsureRegistered()); + + ComPtr toastStatics; + RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), &toastStatics)); + + if (s_aumid.empty()) { + return toastStatics->CreateToastNotifier(notifier); + } else { + return toastStatics->CreateToastNotifierWithId(HStringReference(s_aumid.c_str()).Get(), notifier); + } +} + +HRESULT CreateXmlDocumentFromString(const wchar_t *xmlString, IXmlDocument **doc) { + ComPtr answer; + RETURN_IF_FAILED(Windows::Foundation::ActivateInstance( + HStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument).Get(), &answer)); + + ComPtr docIO; + RETURN_IF_FAILED(answer.As(&docIO)); + + // Load the XML string + RETURN_IF_FAILED(docIO->LoadXml(HStringReference(xmlString).Get())); + + return answer.CopyTo(doc); +} + +HRESULT CreateToastNotification(IXmlDocument *content, IToastNotification **notification) { + ComPtr factory; + RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), &factory)); + + return factory->CreateToastNotification(content, notification); +} + +HRESULT get_History(std::unique_ptr *history) { + RETURN_IF_FAILED(EnsureRegistered()); + + ComPtr toastStatics; + RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), &toastStatics)); + + ComPtr toastStatics2; + RETURN_IF_FAILED(toastStatics.As(&toastStatics2)); + + ComPtr nativeHistory; + RETURN_IF_FAILED(toastStatics2->get_History(&nativeHistory)); + + *history = std::unique_ptr( + new DesktopNotificationHistoryCompat(s_aumid.c_str(), nativeHistory)); + return S_OK; +} + +bool CanUseHttpImages() { + return IsRunningAsUwp(); +} + +HRESULT EnsureRegistered() { + // If not registered AUMID yet + if (!s_registeredAumidAndComServer) { + // Check if Desktop Bridge + if (IsRunningAsUwp()) { + // Implicitly registered, all good! + s_registeredAumidAndComServer = true; + } else { + // Otherwise, incorrect usage, must call RegisterAumidAndComServer first + return E_ILLEGAL_METHOD_CALL; + } + } + + // If not registered activator yet + if (!s_registeredActivator) { + // Incorrect usage, must call RegisterActivator first + return E_ILLEGAL_METHOD_CALL; + } + + return S_OK; +} + +bool IsRunningAsUwp() { + if (!s_hasCheckedIsRunningAsUwp) { + // https://stackoverflow.com/questions/39609643/determine-if-c-application-is-running-as-a-uwp-app-in-desktop-bridge-project + UINT32 length; + wchar_t packageFamilyName[PACKAGE_FAMILY_NAME_MAX_LENGTH + 1]; + LONG result = GetPackageFamilyName(GetCurrentProcess(), &length, packageFamilyName); + s_isRunningAsUwp = result == ERROR_SUCCESS; + s_hasCheckedIsRunningAsUwp = true; + } + + return s_isRunningAsUwp; +} +} // namespace DesktopNotificationManagerCompat + +DesktopNotificationHistoryCompat::DesktopNotificationHistoryCompat(const wchar_t *aumid, + ComPtr history) { + m_aumid = std::wstring(aumid); + m_history = history; +} + +HRESULT DesktopNotificationHistoryCompat::Clear() { + if (m_aumid.empty()) { + return m_history->Clear(); + } else { + return m_history->ClearWithId(HStringReference(m_aumid.c_str()).Get()); + } +} + +HRESULT DesktopNotificationHistoryCompat::GetHistory( + ABI::Windows::Foundation::Collections::IVectorView **toasts) { + ComPtr history2; + RETURN_IF_FAILED(m_history.As(&history2)); + + if (m_aumid.empty()) { + return history2->GetHistory(toasts); + } else { + return history2->GetHistoryWithId(HStringReference(m_aumid.c_str()).Get(), toasts); + } +} + +HRESULT DesktopNotificationHistoryCompat::Remove(const wchar_t *tag) { + if (m_aumid.empty()) { + return m_history->Remove(HStringReference(tag).Get()); + } else { + return m_history->RemoveGroupedTagWithId(HStringReference(tag).Get(), HStringReference(L"").Get(), + HStringReference(m_aumid.c_str()).Get()); + } +} + +HRESULT DesktopNotificationHistoryCompat::RemoveGroupedTag(const wchar_t *tag, const wchar_t *group) { + if (m_aumid.empty()) { + return m_history->RemoveGroupedTag(HStringReference(tag).Get(), HStringReference(group).Get()); + } else { + return m_history->RemoveGroupedTagWithId(HStringReference(tag).Get(), HStringReference(group).Get(), + HStringReference(m_aumid.c_str()).Get()); + } +} + +HRESULT DesktopNotificationHistoryCompat::RemoveGroup(const wchar_t *group) { + if (m_aumid.empty()) { + return m_history->RemoveGroup(HStringReference(group).Get()); + } else { + return m_history->RemoveGroupWithId(HStringReference(group).Get(), HStringReference(m_aumid.c_str()).Get()); + } +} diff --git a/Linphone/core/notifier/DesktopNotificationManagerCompat.hpp b/Linphone/core/notifier/DesktopNotificationManagerCompat.hpp new file mode 100644 index 000000000..5f731b089 --- /dev/null +++ b/Linphone/core/notifier/DesktopNotificationManagerCompat.hpp @@ -0,0 +1,112 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED ?AS IS?, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +#pragma once +#include +#include +#include +#include +#include +#define TOAST_ACTIVATED_LAUNCH_ARG L"-ToastActivated" + +using namespace ABI::Windows::UI::Notifications; + +class DesktopNotificationHistoryCompat; + +namespace DesktopNotificationManagerCompat { + +/// Froce shortcut creation +HRESULT CreateStartMenuShortcut(const wchar_t *aumid, GUID clsid); + +/// +/// If not running under the Desktop Bridge, you must call this method to register your AUMID with the Compat library +/// and to register your COM CLSID and EXE in LocalServer32 registry. Feel free to call this regardless, and we will +/// no-op if running under Desktop Bridge. Call this upon application startup, before calling any other APIs. +/// +/// An AUMID that uniquely identifies your application. +/// The CLSID of your NotificationActivator class. +HRESULT RegisterAumidAndComServer(const wchar_t *aumid, GUID clsid); + +/// +/// Registers your module to handle COM activations. Call this upon application startup. +/// +HRESULT RegisterActivator(); + +/// +/// Creates a toast notifier. You must have called RegisterActivator first (and also RegisterAumidAndComServer if you're +/// a classic Win32 app), or this will throw an exception. +/// +HRESULT CreateToastNotifier(IToastNotifier **notifier); + +/// +/// Creates an XmlDocument initialized with the specified string. This is simply a convenience helper method. +/// +HRESULT CreateXmlDocumentFromString(const wchar_t *xmlString, ABI::Windows::Data::Xml::Dom::IXmlDocument **doc); + +/// +/// Creates a toast notification. This is simply a convenience helper method. +/// +HRESULT CreateToastNotification(ABI::Windows::Data::Xml::Dom::IXmlDocument *content, IToastNotification **notification); + +/// +/// Gets the DesktopNotificationHistoryCompat object. You must have called RegisterActivator first (and also +/// RegisterAumidAndComServer if you're a classic Win32 app), or this will throw an exception. +/// +HRESULT get_History(std::unique_ptr *history); + +/// +/// Gets a boolean representing whether http images can be used within toasts. This is true if running under Desktop +/// Bridge. +/// +bool CanUseHttpImages(); +} // namespace DesktopNotificationManagerCompat + +class DesktopNotificationHistoryCompat { +public: + /// + /// Removes all notifications sent by this app from action center. + /// + HRESULT Clear(); + + /// + /// Gets all notifications sent by this app that are currently still in Action Center. + /// + HRESULT GetHistory(ABI::Windows::Foundation::Collections::IVectorView **history); + + /// + /// Removes an individual toast, with the specified tag label, from action center. + /// + /// The tag label of the toast notification to be removed. + HRESULT Remove(const wchar_t *tag); + + /// + /// Removes a toast notification from the action using the notification's tag and group labels. + /// + /// The tag label of the toast notification to be removed. + /// The group label of the toast notification to be removed. + HRESULT RemoveGroupedTag(const wchar_t *tag, const wchar_t *group); + + /// + /// Removes a group of toast notifications, identified by the specified group label, from action center. + /// + /// The group label of the toast notifications to be removed. + HRESULT RemoveGroup(const wchar_t *group); + + /// + /// Do not call this. Instead, call DesktopNotificationManagerCompat.get_History() to obtain an instance. + /// + DesktopNotificationHistoryCompat(const wchar_t *aumid, Microsoft::WRL::ComPtr history); + +private: + std::wstring m_aumid; + Microsoft::WRL::ComPtr m_history = nullptr; +}; diff --git a/Linphone/core/notifier/NotificationActivator.cpp b/Linphone/core/notifier/NotificationActivator.cpp new file mode 100644 index 000000000..230fa6ffc --- /dev/null +++ b/Linphone/core/notifier/NotificationActivator.cpp @@ -0,0 +1,20 @@ +#include "NotificationActivator.hpp" + +NotificationActivator::NotificationActivator() { +} + +NotificationActivator::~NotificationActivator() { +} + +HRESULT STDMETHODCALLTYPE NotificationActivator::Activate(LPCWSTR appUserModelId, + LPCWSTR invokedArgs, + const NOTIFICATION_USER_INPUT_DATA *data, + ULONG dataCount) { + Q_UNUSED(appUserModelId); + Q_UNUSED(invokedArgs); + Q_UNUSED(data); + Q_UNUSED(dataCount); + return S_OK; +} + +CoCreatableClass(NotificationActivator); diff --git a/Linphone/core/notifier/NotificationActivator.hpp b/Linphone/core/notifier/NotificationActivator.hpp new file mode 100644 index 000000000..7754647b8 --- /dev/null +++ b/Linphone/core/notifier/NotificationActivator.hpp @@ -0,0 +1,42 @@ +#ifndef NOTIFICATIONACTIVATOR_HPP +#define NOTIFICATIONACTIVATOR_HPP + +#pragma once +#include +#include +#include +#include +#include +#include +#include +// #include +#include +#include +// #include + +// using namespace winrt; +// using namespace Windows::UI::Notifications; +// using namespace Windows::Data::Xml::Dom; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace Microsoft::WRL; + +using Microsoft::WRL::ClassicCom; +using Microsoft::WRL::RuntimeClass; +using Microsoft::WRL::RuntimeClassFlags; + +class DECLSPEC_UUID("FC946101-E4AB-4EA4-BC2E-C7F4D72B89AC") NotificationActivator + : public Microsoft::WRL::RuntimeClass, + INotificationActivationCallback> { + +public: + NotificationActivator(); + ~NotificationActivator(); + + static void onActivated(LPCWSTR invokedArgs); // appelé depuis le .cpp + HRESULT STDMETHODCALLTYPE Activate(LPCWSTR appUserModelId, + LPCWSTR invokedArgs, + const NOTIFICATION_USER_INPUT_DATA *data, + ULONG dataCount) override; +}; + +#endif // NOTIFICATIONACTIVATOR_HPP diff --git a/Linphone/core/notifier/Notifier.cpp b/Linphone/core/notifier/Notifier.cpp index 73ee52865..9ed0eee00 100644 --- a/Linphone/core/notifier/Notifier.cpp +++ b/Linphone/core/notifier/Notifier.cpp @@ -30,15 +30,13 @@ #include #include "Notifier.hpp" +#include "WindowsNotificationBackend.hpp" #include "core/App.hpp" #include "core/call/CallGui.hpp" #include "core/chat/ChatGui.hpp" #include "model/tool/ToolModel.hpp" -#include "tool/LinphoneEnums.hpp" #include "tool/accessibility/AccessibilityHelper.hpp" -#include "tool/providers/AvatarProvider.hpp" -#include "tool/providers/ImageProvider.hpp" DEFINE_ABSTRACT_OBJECT(Notifier) @@ -87,9 +85,12 @@ void setProperty(QObject &object, const char *property, const T &value) { // ============================================================================= const QHash Notifier::Notifications = { - {Notifier::ReceivedMessage, {Notifier::ReceivedMessage, "NotificationReceivedMessage.qml", 10}}, - //{Notifier::ReceivedFileMessage, {Notifier::ReceivedFileMessage, "NotificationReceivedFileMessage.qml", 10}}, - {Notifier::ReceivedCall, {Notifier::ReceivedCall, "NotificationReceivedCall.qml", 30}} + {AbstractNotificationBackend::ReceivedMessage, + {AbstractNotificationBackend::ReceivedMessage, "NotificationReceivedMessage.qml", 10}}, + //{AbstractNotificationBackend::ReceivedFileMessage, {AbstractNotificationBackend::ReceivedFileMessage, + //"NotificationReceivedFileMessage.qml", 10}}, + {AbstractNotificationBackend::ReceivedCall, + {AbstractNotificationBackend::ReceivedCall, "NotificationReceivedCall.qml", 30}} //{Notifier::NewVersionAvailable, {Notifier::NewVersionAvailable, "NotificationNewVersionAvailable.qml", 30}}, //{Notifier::SnapshotWasTaken, {Notifier::SnapshotWasTaken, "NotificationSnapshotWasTaken.qml", 10}}, //{Notifier::RecordingCompleted, {Notifier::RecordingCompleted, "NotificationRecordingCompleted.qml", 10}} @@ -98,7 +99,7 @@ const QHash Notifier::Notifications = { // ----------------------------------------------------------------------------- Notifier::Notifier(QObject *parent) : QObject(parent) { - mustBeInMainThread(getClassName()); + mustBeInMainThread("Notifier"); const int nComponents = Notifications.size(); mComponents.resize(nComponents); @@ -118,113 +119,123 @@ Notifier::Notifier(QObject *parent) : QObject(parent) { } Notifier::~Notifier() { - mustBeInMainThread("~" + getClassName()); + mustBeInMainThread("~Notifier"); delete mMutex; - const int nComponents = Notifications.size(); mComponents.clear(); } // ----------------------------------------------------------------------------- -bool Notifier::createNotification(Notifier::NotificationType type, QVariantMap data) { +bool Notifier::createNotification(AbstractNotificationBackend::NotificationType type, QVariantMap data) { mMutex->lock(); // Q_ASSERT(mInstancesNumber <= MaxNotificationsNumber); - if (mInstancesNumber >= MaxNotificationsNumber) { // Check existing instances. - qWarning() << QStringLiteral("Unable to create another notification."); +#ifdef Q_OS_WIN + + auto notifBackend = App::getInstance()->getNotificationBackend(); + if (!notifBackend) { + qWarning() << QStringLiteral("Unable to get notification backend, return."); mMutex->unlock(); return false; } - QList allScreens = QGuiApplication::screens(); - if (allScreens.size() > 0) { // Ensure to have a screen to avoid errors - QQuickItem *previousWrapper = nullptr; - bool showAsTool = false; + notifBackend->sendNotification(type, data); +#else + + if (mInstancesNumber >= MaxNotificationsNumber) { // Check existing instances. + qWarning() << QStringLiteral("Unable to create another notification."); + QList allScreens = QGuiApplication::screens(); + if (allScreens.size() > 0) { // Ensure to have a screen to avoid errors + QQuickItem *previousWrapper = nullptr; + bool showAsTool = false; #ifdef Q_OS_MACOS - for (auto w : QGuiApplication::topLevelWindows()) { - if (w->visibility() == QWindow::FullScreen) { - showAsTool = true; - w->raise(); // Used to get focus on Mac (On Mac, A Tool is hidden if the app has not focus and the - // only way to rid it is to use Widget Attributes(Qt::WA_MacAlwaysShowToolWindow) that is not available) + for (auto w : QGuiApplication::topLevelWindows()) { + if (w->visibility() == QWindow::FullScreen) { + showAsTool = true; + w->raise(); // Used to get focus on Mac (On Mac, A Tool is hidden if the app has not focus and the + // only way to rid it is to use Widget Attributes(Qt::WA_MacAlwaysShowToolWindow) that is not + // available) + } + } +#endif + for (int i = 0; i < allScreens.size(); ++i) { + + ++mInstancesNumber; + // Use QQuickView to create a visual root object that is + // independant from current application Window + QScreen *screen = allScreens[i]; + auto engine = App::getInstance()->mEngine; + const QUrl url(QString(NotificationsPath) + Notifier::Notifications[type].filename); + QObject::connect( + engine, &QQmlApplicationEngine::objectCreated, this, + [this, url, screen, engine, type, data, showAsTool](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) { + lCritical() << "[App] Notifier.qml couldn't be load."; + engine->deleteLater(); + exit(-1); + } else { + auto window = qobject_cast(obj); + if (window) { + window->setParent(nullptr); + window->setProperty(NotificationPropertyData, data); + window->setScreen(screen); + // Don't use Popup for flags : it could lead to error in geometry. On Mac, Using Tool + // ensure to have the Window on Top and fullscreen independant + window->setFlags((showAsTool ? Qt::Tool : Qt::WindowStaysOnTopHint) | + Qt::FramelessWindowHint); +#if defined(Q_OS_LINUX) || defined(Q_OS_WIN) + window->setFlag(Qt::WindowDoesNotAcceptFocus); +#endif + // for (auto it = data.begin(); it != data.end(); ++it) + // window->setProperty(it.key().toLatin1(), it.value()); + const int timeout = Notifications[type].getTimeout() * 1000; + auto updateNotificationCoordinates = [this, window, screen](int width, int height) { + auto screenHeightOffset = + screen ? mScreenHeightOffset.value(screen->name()) : 0; // Access optimization + QRect availableGeometry = screen->availableGeometry(); + + window->setX(availableGeometry.x() + + (availableGeometry.width() - + width)); //*screen->devicePixelRatio()); when using manual scaler + window->setY(availableGeometry.y() + availableGeometry.height() - + screenHeightOffset - height); + }; + updateNotificationCoordinates(window->width(), window->height()); + auto screenHeightOffset = + screen ? mScreenHeightOffset.take(screen->name()) : 0; // Access optimization + mScreenHeightOffset.insert(screen->name(), screenHeightOffset + window->height()); + QObject::connect(window, &QQuickWindow::closing, window, [this, window] { + qDebug() << "closing notification"; + deleteNotification(QVariant::fromValue(window)); + }); + QObject::connect(window, &QQuickWindow::visibleChanged, window, [this](bool visible) { + lInfo() << log().arg("Notification visible changed") << visible; + }); + QObject::connect(window, &QQuickWindow::activeChanged, window, [this, window] { + lInfo() << log().arg("Notification active changed") << window->isActive(); + }); + QObject::connect(window, &QQuickWindow::visibilityChanged, window, + [this](QWindow::Visibility visibility) { + lInfo() + << log().arg("Notification visibility changed") << visibility; + }); + QObject::connect(window, &QQuickWindow::widthChanged, window, [this](int width) { + lInfo() << log().arg("Notification width changed") << width; + }); + QObject::connect(window, &QQuickWindow::heightChanged, window, [this](int height) { + lInfo() << log().arg("Notification height changed") << height; + }); + lInfo() << QStringLiteral("Create notification:") << window; + showNotification(window, timeout); + } + } + }, + static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); + lDebug() << log().arg("Engine loading notification"); + engine->load(url); } } -#endif - for (int i = 0; i < allScreens.size(); ++i) { - - ++mInstancesNumber; - // Use QQuickView to create a visual root object that is - // independant from current application Window - QScreen *screen = allScreens[i]; - auto engine = App::getInstance()->mEngine; - const QUrl url(QString(NotificationsPath) + Notifier::Notifications[type].filename); - QObject::connect( - engine, &QQmlApplicationEngine::objectCreated, this, - [this, url, screen, engine, type, data, showAsTool](QObject *obj, const QUrl &objUrl) { - if (!obj && url == objUrl) { - lCritical() << "[App] Notifier.qml couldn't be load."; - engine->deleteLater(); - exit(-1); - } else { - auto window = qobject_cast(obj); - if (window) { - window->setParent(nullptr); - window->setProperty(NotificationPropertyData, data); - window->setScreen(screen); - // Don't use Popup for flags : it could lead to error in geometry. On Mac, Using Tool ensure - // to have the Window on Top and fullscreen independant - window->setFlags((showAsTool ? Qt::Tool : Qt::WindowStaysOnTopHint) | - Qt::FramelessWindowHint); -#if defined(Q_OS_LINUX) || defined(Q_OS_WIN) - window->setFlag(Qt::WindowDoesNotAcceptFocus); -#endif - // for (auto it = data.begin(); it != data.end(); ++it) - // window->setProperty(it.key().toLatin1(), it.value()); - const int timeout = Notifications[type].getTimeout() * 1000; - auto updateNotificationCoordinates = [this, window, screen](int width, int height) { - auto screenHeightOffset = - screen ? mScreenHeightOffset.value(screen->name()) : 0; // Access optimization - QRect availableGeometry = screen->availableGeometry(); - - window->setX(availableGeometry.x() + - (availableGeometry.width() - - width)); //*screen->devicePixelRatio()); when using manual scaler - window->setY(availableGeometry.y() + availableGeometry.height() - screenHeightOffset - - height); - }; - updateNotificationCoordinates(window->width(), window->height()); - auto screenHeightOffset = - screen ? mScreenHeightOffset.take(screen->name()) : 0; // Access optimization - mScreenHeightOffset.insert(screen->name(), screenHeightOffset + window->height()); - QObject::connect(window, &QQuickWindow::closing, window, [this, window] { - qDebug() << "closing notification"; - deleteNotification(QVariant::fromValue(window)); - }); - QObject::connect(window, &QQuickWindow::visibleChanged, window, [this](bool visible) { - lInfo() << log().arg("Notification visible changed") << visible; - }); - QObject::connect(window, &QQuickWindow::activeChanged, window, [this, window] { - lInfo() << log().arg("Notification active changed") << window->isActive(); - }); - QObject::connect(window, &QQuickWindow::visibilityChanged, window, - [this](QWindow::Visibility visibility) { - lInfo() << log().arg("Notification visibility changed") << visibility; - }); - QObject::connect(window, &QQuickWindow::widthChanged, window, [this](int width) { - lInfo() << log().arg("Notification width changed") << width; - }); - QObject::connect(window, &QQuickWindow::heightChanged, window, [this](int height) { - lInfo() << log().arg("Notification height changed") << height; - }); - lInfo() << QStringLiteral("Create notification:") << window; - showNotification(window, timeout); - } - } - }, - static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); - lDebug() << log().arg("Engine loading notification"); - engine->load(url); - } } - +#endif mMutex->unlock(); return true; } @@ -237,17 +248,6 @@ void Notifier::showNotification(QQuickWindow *notification, int timeout) { timer->setInterval(timeout); timer->setSingleShot(true); notification->setProperty(NotificationPropertyTimer, QVariant::fromValue(timer)); -#ifdef Q_OS_WIN - QObject::connect(App::getInstance(), &App::sessionUnlocked, notification, [this, notification] { - if (!notification) return; - lInfo() << log().arg("Windows : screen unlocked, force raising notification"); - notification->hide(); - notification->showNormal(); - // notification->raise(); - lInfo() << log().arg("Notification visibility : visible =") << notification->isVisible() - << "visibility =" << notification->visibility(); - }); -#endif notification->hide(); notification->showNormal(); // notification->raise(); @@ -322,8 +322,8 @@ void Notifier::notifyReceivedCall(const shared_ptr &call) { if (account) { auto accountModel = Utils::makeQObject_ptr(account); if (!accountModel->getNotificationsAllowed()) { - lInfo() << log().arg( - "Notifications have been disabled for this account - not creating a notification for incoming call"); + lInfo() << log().arg("Notifications have been disabled for this account - not creating a notification " + "for incoming call"); if (accountModel->forwardToVoiceMailInDndPresence()) { lInfo() << log().arg("Transferring call to voicemail"); auto voicemailAddress = linphone::Factory::get()->createAddress( @@ -353,7 +353,8 @@ void Notifier::notifyReceivedCall(const shared_ptr &call) { map["displayName"].setValue(displayName); map["call"].setValue(gui); - CREATE_NOTIFICATION(Notifier::ReceivedCall, map) + + CREATE_NOTIFICATION(AbstractNotificationBackend::ReceivedCall, map) }); } @@ -394,7 +395,7 @@ void Notifier::notifyReceivedMessages(const std::shared_ptr remoteAddress = ToolModel::getDisplayName(remoteAddr); auto fileContent = message->getFileTransferInformation(); if (!fileContent) { - for (auto content : message->getContents()) { + for (const auto &content : message->getContents()) { if (content->isText()) txt += content->getUtf8Text().c_str(); } } else if (fileContent->isVoiceRecording()) @@ -466,7 +467,7 @@ void Notifier::notifyReceivedMessages(const std::shared_ptr map["avatarUri"] = chatCore->getAvatarUri(); map["isGroupChat"] = chatCore->isGroupChat(); map["chat"] = QVariant::fromValue(chatCore ? new ChatGui(chatCore) : nullptr); - CREATE_NOTIFICATION(Notifier::ReceivedMessage, map) + CREATE_NOTIFICATION(AbstractNotificationBackend::ReceivedMessage, map) }); } } diff --git a/Linphone/core/notifier/Notifier.hpp b/Linphone/core/notifier/Notifier.hpp index e64c0d2bc..b0eda3f68 100644 --- a/Linphone/core/notifier/Notifier.hpp +++ b/Linphone/core/notifier/Notifier.hpp @@ -30,6 +30,8 @@ #include // ============================================================================= +#include "core/notifier/AbstractNotificationBackend.hpp" + class QMutex; class QQmlComponent; @@ -40,15 +42,6 @@ public: Notifier(QObject *parent = Q_NULLPTR); ~Notifier(); - enum NotificationType { - ReceivedMessage, - // ReceivedFileMessage, - ReceivedCall - // NewVersionAvailable, - // SnapshotWasTaken, - // RecordingCompleted - }; - // void notifyReceivedCall(Call *call); void notifyReceivedCall(const std::shared_ptr &call); // Call from Linphone @@ -77,7 +70,7 @@ private: this->timeout = timeout; } int getTimeout() const { - if (type == Notifier::ReceivedCall) { + if (type == AbstractNotificationBackend::ReceivedCall) { // return CoreManager::getInstance()->getSettingsModel()->getIncomingCallTimeout(); return 30; } else return timeout; @@ -89,7 +82,7 @@ private: int type; }; - bool createNotification(NotificationType type, QVariantMap data); + bool createNotification(AbstractNotificationBackend::NotificationType type, QVariantMap data); void showNotification(QQuickWindow *notification, int timeout); QHash mScreenHeightOffset; diff --git a/Linphone/core/notifier/WindowsNotificationBackend.cpp b/Linphone/core/notifier/WindowsNotificationBackend.cpp new file mode 100644 index 000000000..ad677f078 --- /dev/null +++ b/Linphone/core/notifier/WindowsNotificationBackend.cpp @@ -0,0 +1,361 @@ +#include "tool/Utils.hpp" + +// #include "NotificationActivator.hpp" +#include "WindowsNotificationBackend.hpp" +#include "core/App.hpp" +#include "core/call/CallGui.hpp" +#include "core/chat/ChatGui.hpp" +#include "core/event-filter/LockEventFilter.hpp" +#include "tool/Utils.hpp" + +#ifdef Q_OS_WIN +#include "DesktopNotificationManagerCompat.hpp" +#include +#include +#endif + +#include + +using namespace Microsoft::WRL; +using namespace ABI::Windows::UI::Notifications; +using namespace ABI::Windows::Foundation; +using namespace Microsoft::WRL::Wrappers; + +NotificationBackend::NotificationBackend(QObject *parent) : AbstractNotificationBackend(parent) { + connect(App::getInstance(), &App::sessionLockedChanged, this, [this] { + if (!App::getInstance()->getSessionLocked()) { + qDebug() << "Session unlocked, flush pending notifications"; + flushPendingNotifications(); + } + }); +} + +void NotificationBackend::flushPendingNotifications() { + for (const auto ¬if : mPendingNotifications) { + sendNotification(notif.type, notif.data); + } + mPendingNotifications.clear(); +} + +void NotificationBackend::sendNotification(const QString &title, + const QString &message, + const QList &actions) { + IToastNotifier *notifier = nullptr; + HRESULT hr = DesktopNotificationManagerCompat::CreateToastNotifier(¬ifier); + if (FAILED(hr) || !notifier) { + lWarning() << "CreateToastNotifier failed:" << Qt::hex << hr; + return; + } + + std::wstring wTitle = title.toStdWString(); + std::wstring wMessage = message.toStdWString(); + + std::wstring wActions; + if (!actions.isEmpty()) { + wActions += L""; + for (const auto &action : actions) { + std::wstring wLabel = action.label.toStdWString(); + std::wstring wArg = action.argument.toStdWString(); + wActions += L""; + } + wActions += L""; + } + + std::wstring xml = L"" + L" " + L" " + L" " + + wTitle + + L"" + L" " + + wMessage + + L"" + L" " + L" " + + wActions + L""; + + ABI::Windows::Data::Xml::Dom::IXmlDocument *doc = nullptr; + hr = DesktopNotificationManagerCompat::CreateXmlDocumentFromString(xml.c_str(), &doc); + if (FAILED(hr) || !doc) { + lWarning() << "CreateXmlDocumentFromString failed:" << Qt::hex << hr; + notifier->Release(); + return; + } + + IToastNotification *toast = nullptr; + hr = DesktopNotificationManagerCompat::CreateToastNotification(doc, &toast); + if (FAILED(hr) || !toast) { + qWarning() << "CreateToastNotification failed:" << Qt::hex << hr; + doc->Release(); + notifier->Release(); + return; + } + + EventRegistrationToken token; + toast->add_Activated(Microsoft::WRL::Callback>( + [this](IToastNotification *sender, IInspectable *args) -> HRESULT { + qInfo() << "Toast clicked!"; + // Cast args en IToastActivatedEventArgs + Microsoft::WRL::ComPtr activatedArgs; + HRESULT hr = args->QueryInterface(IID_PPV_ARGS(&activatedArgs)); + if (SUCCEEDED(hr)) { + HSTRING argumentsHString; + activatedArgs->get_Arguments(&argumentsHString); + + // Convertir HSTRING en wstring + UINT32 length; + const wchar_t *rawStr = WindowsGetStringRawBuffer(argumentsHString, &length); + std::wstring arguments(rawStr, length); + + qInfo() << "Toast activated with args:" << QString::fromStdWString(arguments); + emit toastActivated(QString::fromStdWString(arguments)); + + WindowsDeleteString(argumentsHString); + } + return S_OK; + }) + .Get(), + &token); + + hr = notifier->Show(toast); + lInfo() << "Show toast:" << Qt::hex << hr; + + if (FAILED(hr)) { + qWarning() << "Toast Show failed:" << Qt::hex << hr; + } + + toast->Release(); + doc->Release(); + notifier->Release(); +} + +void NotificationBackend::sendMessageNotification(QVariantMap data) { + + IToastNotifier *notifier = nullptr; + HRESULT hr = DesktopNotificationManagerCompat::CreateToastNotifier(¬ifier); + if (FAILED(hr) || !notifier) { + lWarning() << "CreateToastNotifier failed:" << Qt::hex << hr; + return; + } + + auto msgTxt = data["message"].toString().toStdWString(); + auto remoteAddress = data["remoteAddress"].toString().toStdWString(); + auto chatRoomName = data["chatRoomName"].toString().toStdWString(); + auto chatRoomAddress = data["chatRoomAddress"].toString().toStdWString(); + auto avatarUri = data["avatarUri"].toString().toStdWString(); + bool isGroup = data["isGroupChat"].toBool(); + ChatGui *chat = data["chat"].value(); + + std::wstring xml = L"" + L" " + L" " + L" " + L" " + L" " + L" " + L" " + L" " + L" " + L" " + L" " + L" "; + + ABI::Windows::Data::Xml::Dom::IXmlDocument *doc = nullptr; + hr = DesktopNotificationManagerCompat::CreateXmlDocumentFromString(xml.c_str(), &doc); + if (FAILED(hr) || !doc) { + lWarning() << "CreateXmlDocumentFromString failed:" << Qt::hex << hr; + notifier->Release(); + return; + } + + IToastNotification *toast = nullptr; + hr = DesktopNotificationManagerCompat::CreateToastNotification(doc, &toast); + if (FAILED(hr) || !toast) { + qWarning() << "CreateToastNotification failed:" << Qt::hex << hr; + doc->Release(); + notifier->Release(); + Utils::showInformationPopup(tr("info_popup_error_title"), tr("info_popup_error_creating_notification"), false); + return; + } + + EventRegistrationToken token; + toast->add_Activated(Microsoft::WRL::Callback>( + [this, chat](IToastNotification *sender, IInspectable *args) -> HRESULT { + qInfo() << "Message toast clicked!"; + + Utils::openChat(chat); + + return S_OK; + }) + .Get(), + &token); + + hr = notifier->Show(toast); + if (FAILED(hr)) { + qWarning() << "Toast Show failed:" << Qt::hex << hr; + } + + toast->Release(); + doc->Release(); + notifier->Release(); +} + +void NotificationBackend::sendCallNotification(QVariantMap data) { + + IToastNotifier *notifier = nullptr; + HRESULT hr = DesktopNotificationManagerCompat::CreateToastNotifier(¬ifier); + if (FAILED(hr) || !notifier) { + lWarning() << "CreateToastNotifier failed:" << Qt::hex << hr; + return; + } + + auto displayName = data["displayName"].toString().toStdWString(); + CallGui *call = data["call"].value(); + int timeout = 2; + // AbstractNotificationBackend::Notifications[(int)NotificationType::ReceivedCall].getTimeout(); + + // Incoming call + auto callDescription = tr("incoming_call").toStdWString(); + QList actions; + actions.append(ToastButton(tr("accept_button"), "accept")); + actions.append(ToastButton(tr("decline_button"), "decline")); + std::wstring wActions; + if (!actions.isEmpty()) { + wActions += L""; + for (const auto &action : actions) { + std::wstring wLabel = action.label.toStdWString(); + std::wstring wArg = action.argument.toStdWString(); + wActions += L""; + } + wActions += L""; + } + + std::wstring xml = L"" + L" " + L" " + L" " + + displayName + + L"" + L" " + + callDescription + + L"" + L" " + L" " + + wActions + + L" "; + + ABI::Windows::Data::Xml::Dom::IXmlDocument *doc = nullptr; + hr = DesktopNotificationManagerCompat::CreateXmlDocumentFromString(xml.c_str(), &doc); + if (FAILED(hr) || !doc) { + lWarning() << "CreateXmlDocumentFromString failed:" << Qt::hex << hr; + notifier->Release(); + return; + } + + IToastNotification *toast = nullptr; + hr = DesktopNotificationManagerCompat::CreateToastNotification(doc, &toast); + if (FAILED(hr) || !toast) { + qWarning() << "CreateToastNotification failed:" << Qt::hex << hr; + doc->Release(); + notifier->Release(); + Utils::showInformationPopup(tr("info_popup_error_title"), tr("info_popup_error_creating_notification"), false); + return; + } + + ComPtr toast2; + hr = toast->QueryInterface(IID_PPV_ARGS(&toast2)); + if (FAILED(hr)) qWarning() << "QueryInterface failed"; + auto callId = call->mCore->getCallId(); + qDebug() << "put tag to toast" << callId; + hr = toast2->put_Tag(HStringReference(reinterpret_cast(callId.utf16())).Get()); + toast2->put_Group(HStringReference(L"linphone").Get()); + if (FAILED(hr)) qWarning() << "puting tag on toast failed"; + + connect(call->mCore.get(), &CallCore::stateChanged, this, [this, call, notifier, toast] { + if (call->mCore->getState() == LinphoneEnums::CallState::End) { + qDebug() << "Call ended, remove toast"; + auto callId = call->mCore->getCallId(); + call->deleteLater(); + + std::unique_ptr history; + DesktopNotificationManagerCompat::get_History(&history); + + auto hr = history->RemoveGroupedTag(reinterpret_cast(callId.utf16()), L"linphone"); + if (FAILED(hr)) { + qWarning() << "removing toast failed"; + } + } + }); + + EventRegistrationToken token; + toast->add_Activated(Microsoft::WRL::Callback>( + [this, call](IToastNotification *sender, IInspectable *args) -> HRESULT { + qInfo() << "Toast clicked!"; + // Cast args en IToastActivatedEventArgs + Microsoft::WRL::ComPtr activatedArgs; + HRESULT hr = args->QueryInterface(IID_PPV_ARGS(&activatedArgs)); + if (SUCCEEDED(hr)) { + HSTRING argumentsHString; + activatedArgs->get_Arguments(&argumentsHString); + + // Convertir HSTRING en wstring + UINT32 length; + const wchar_t *rawStr = WindowsGetStringRawBuffer(argumentsHString, &length); + std::wstring arguments(rawStr, length); + QString arg = QString::fromStdWString(arguments); + qInfo() << "Toast activated with args:" << arg; + + if (arg.compare("accept") == 0) { + if (call) { + qDebug() << "Accept call"; + Utils::openCallsWindow(call); + call->mCore->lAccept(false); + } + } else if (arg.compare("decline") == 0) { + if (call) { + qDebug() << "Decline call"; + call->mCore->lDecline(); + } + } else if (arg.isEmpty()) { + if (call) Utils::openCallsWindow(call); + } + + WindowsDeleteString(argumentsHString); + } + return S_OK; + }) + .Get(), + &token); + + hr = notifier->Show(toast); + if (FAILED(hr)) { + qWarning() << "Toast Show failed:" << Qt::hex << hr; + } + + toast->Release(); + doc->Release(); + notifier->Release(); +} + +void NotificationBackend::sendNotification(NotificationType type, QVariantMap data) { + if (App::getInstance()->getSessionLocked()) { + mPendingNotifications.append({type, data}); + return; + } + switch (type) { + case NotificationType::ReceivedCall: + sendCallNotification(data); + break; + case NotificationType::ReceivedMessage: + sendMessageNotification(data); + break; + } +} diff --git a/Linphone/core/notifier/WindowsNotificationBackend.hpp b/Linphone/core/notifier/WindowsNotificationBackend.hpp new file mode 100644 index 000000000..ffdd565cb --- /dev/null +++ b/Linphone/core/notifier/WindowsNotificationBackend.hpp @@ -0,0 +1,41 @@ +#ifndef WINDOWSNOTIFICATIONBACKEND_HPP +#define WINDOWSNOTIFICATIONBACKEND_HPP + +#include "AbstractNotificationBackend.hpp" +#include +#include +#include +#include + +class NotificationBackend : public AbstractNotificationBackend { + + Q_OBJECT +public: + struct PendingNotification { + NotificationType type; + QVariantMap data; + }; + + NotificationBackend(QObject *parent = nullptr); + ~NotificationBackend() = default; + void sendNotification(const QString &title = QString(), + const QString &message = QString(), + const QList &actions = {}) override; + + void sendCallNotification(QVariantMap data); + void sendMessageNotification(QVariantMap data); + // void sendMessageNotification(QVariantMap data); + + void sendNotification(NotificationType type, QVariantMap data) override; + + void flushPendingNotifications(); + +signals: + void toastButtonTriggered(const QString &arg); + void sessionLockedChanged(bool locked); + +private: + QList mPendingNotifications; +}; + +#endif // WINDOWSNOTIFICATIONBACKEND_HPP diff --git a/Linphone/main.cpp b/Linphone/main.cpp index b791ff183..add2193fc 100644 --- a/Linphone/main.cpp +++ b/Linphone/main.cpp @@ -1,35 +1,37 @@ -#include -#include +#include + +#ifdef _WIN32 +#include +FILE *gStream = NULL; +#include // StringFromCLSID, CoTaskMemFree +#include // PKEY_AppUserModel_ID, PKEY_AppUserModel_ToastActivatorCLSID +#include // InitPropVariantFromString, PropVariantClear +#include // CLSID_ShellLink +#include // IShellLinkW, IPropertyStore +#endif #include "core/App.hpp" #include "core/logger/QtLogger.hpp" #include "core/path/Paths.hpp" +#include "core/notifier/DesktopNotificationManagerCompat.hpp" +#include "core/notifier/NotificationActivator.hpp" + +#include #include +#include #include #include -#include -#include + #ifdef QT_QML_DEBUG #include +#include #endif -#ifdef _WIN32 -#include -FILE *gStream = NULL; -#endif +#define WIDEN2(x) L##x +#define WIDEN(x) WIDEN2(x) -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 10) -// From 5.15.2 to 5.15.10, sometimes, Accessibility freeze the application : Deactivate handlers. -#define ACCESSBILITY_WORKAROUND -#include -#include - -void DummyUpdateHandler(QAccessibleEvent *event) { -} -void DummyRootObjectHandler(QObject *) { -} -#endif +static const wchar_t *mAumid = WIDEN(APPLICATION_ID); void cleanStream() { #ifdef _WIN32 @@ -53,6 +55,19 @@ int main(int argc, char *argv[]) { #endif */ + // auto hrCom = RoInitialize(RO_INIT_MULTITHREADED); + // HRESULT hrCom = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + // qInfo() << "CoInitializeEx result:" << Qt::hex << hrCom; + + qInfo() << "Thread ID:" << GetCurrentThreadId(); + APTTYPE aptBefore; + APTTYPEQUALIFIER qualBefore; + CoGetApartmentType(&aptBefore, &qualBefore); + qInfo() << "ApartmentType BEFORE CoInitializeEx:" << aptBefore; + + HRESULT hrCom = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + qInfo() << "CoInitializeEx STA result:" << Qt::hex << hrCom; + // Useful to share camera on Fullscreen (other context) or multiscreens lDebug() << "[Main] Setting ShareOpenGLContexts"; QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); @@ -67,8 +82,25 @@ int main(int argc, char *argv[]) { lDebug() << "[Main] Setting application to UTF8"; setlocale(LC_CTYPE, ".UTF8"); lDebug() << "[Main] Creating application"; + + auto hr = DesktopNotificationManagerCompat::CreateStartMenuShortcut(mAumid, __uuidof(NotificationActivator)); + if (FAILED(hr)) { + qWarning() << "CreateStartMenuShortcut failed:" << Qt::hex << hr; + } + + // Register AUMID and COM server (for a packaged app, this is a no-operation) + hr = DesktopNotificationManagerCompat::RegisterAumidAndComServer(L"Linphone", __uuidof(NotificationActivator)); + if (FAILED(hr)) { + qWarning() << "RegisterAumidAndComServer failed:" << Qt::hex << hr; + } + auto app = QSharedPointer::create(argc, argv); + hr = DesktopNotificationManagerCompat::RegisterActivator(); + if (FAILED(hr)) { + qWarning() << "RegisterActivator failed:" << Qt::hex << hr; + } + #ifdef ACCESSBILITY_WORKAROUND QAccessible::installUpdateHandler(DummyUpdateHandler); QAccessible::installRootObjectHandler(DummyRootObjectHandler); diff --git a/Linphone/model/core/CoreModel.hpp b/Linphone/model/core/CoreModel.hpp index fefbba76c..bf04f043c 100644 --- a/Linphone/model/core/CoreModel.hpp +++ b/Linphone/model/core/CoreModel.hpp @@ -40,7 +40,6 @@ // ============================================================================= -class AbstractEventCountNotifier; class EventCountNotifier; class CoreModel : public ::Listener, diff --git a/Linphone/view/Control/Popup/Notification/NotificationReceivedMessage.qml b/Linphone/view/Control/Popup/Notification/NotificationReceivedMessage.qml index a5a8a5484..ea025d8e3 100644 --- a/Linphone/view/Control/Popup/Notification/NotificationReceivedMessage.qml +++ b/Linphone/view/Control/Popup/Notification/NotificationReceivedMessage.qml @@ -21,7 +21,6 @@ Notification { property string avatarUri: notificationData?.avatarUri property string chatRoomName: notificationData?.chatRoomName ? notificationData.chatRoomName : "" property string remoteAddress: notificationData?.remoteAddress ? notificationData.remoteAddress : "" - property string chatRoomAddress: notificationData?.chatRoomAddress ? notificationData.chatRoomAddress : "" property bool isGroupChat: notificationData?.isGroupChat ? notificationData.isGroupChat : false property string message: notificationData?.message ? notificationData.message : "" Connections { diff --git a/Linphone/view/Page/Window/Call/CallsWindow.qml b/Linphone/view/Page/Window/Call/CallsWindow.qml index 13011c95e..c4be4dcc1 100644 --- a/Linphone/view/Page/Window/Call/CallsWindow.qml +++ b/Linphone/view/Page/Window/Call/CallsWindow.qml @@ -1373,7 +1373,8 @@ AbstractWindow { connectedCallButtons.visible = bottomButtonsLayout.visible moreOptionsButtonVisibility = bottomButtonsLayout.visible bottomButtonsLayout.layoutDirection = Qt.RightToLeft - } else if (mainWindow.callState === LinphoneEnums.CallState.OutgoingInit) { + } else if (mainWindow.callState === LinphoneEnums.CallState.OutgoingInit + || mainWindow.callState === LinphoneEnums.CallState.IncomingReceived) { connectedCallButtons.visible = false bottomButtonsLayout.layoutDirection = Qt.LeftToRight moreOptionsButtonVisibility = false @@ -1396,28 +1397,52 @@ AbstractWindow { } // End call button - BigButton { - id: endCallButton - focus: true - Layout.row: 0 - icon.width: Utils.getSizeWithScreenRatio(32) - icon.height: Utils.getSizeWithScreenRatio(32) - //: "Terminer l'appel" - ToolTip.text: qsTr("call_action_end_call") - Accessible.name: qsTr("call_action_end_call") - Layout.preferredWidth: Utils.getSizeWithScreenRatio(75) - Layout.preferredHeight: Utils.getSizeWithScreenRatio(55) - radius: Utils.getSizeWithScreenRatio(71) - style: ButtonStyle.phoneRedLightBorder - Layout.column: mainWindow.startingCall ? 0 : bottomButtonsLayout.columns - 1 - KeyNavigation.tab: mainWindow.startingCall ? (videoCameraButton.visible && videoCameraButton.enabled ? videoCameraButton : audioMicrophoneButton) : openStatisticPanelButton - KeyNavigation.backtab: mainWindow.startingCall ? rightPanel.visible ? Utils.getLastFocusableItemInItem(rightPanel) : nextItemInFocusChain(false): callListButton - onClicked: { - mainWindow.callTerminatedByUser = true - mainWindow.endCall(mainWindow.call) + RowLayout { + spacing: Utils.getSizeWithScreenRatio(10) + BigButton { + id: acceptCallButton + visible: mainWindow.callState === LinphoneEnums.CallState.IncomingReceived + Layout.row: 0 + icon.width: Utils.getSizeWithScreenRatio(32) + icon.height: Utils.getSizeWithScreenRatio(32) + //: "Accepter l'appel" + ToolTip.text: qsTr("call_action_accept_call") + Accessible.name: qsTr("call_action_accept_call") + Layout.preferredWidth: Utils.getSizeWithScreenRatio(75) + Layout.preferredHeight: Utils.getSizeWithScreenRatio(55) + radius: Utils.getSizeWithScreenRatio(71) + style: ButtonStyle.phoneGreenLightBorder + Layout.column: mainWindow.startingCall ? 0 : bottomButtonsLayout.columns - 1 + KeyNavigation.tab: mainWindow.startingCall ? (videoCameraButton.visible && videoCameraButton.enabled ? videoCameraButton : audioMicrophoneButton) : openStatisticPanelButton + KeyNavigation.backtab:endCallButton + onClicked: { + mainWindow.call.core.lAccept(false) + } + } + BigButton { + id: endCallButton + focus: true + Layout.row: 0 + icon.width: Utils.getSizeWithScreenRatio(32) + icon.height: Utils.getSizeWithScreenRatio(32) + //: "Terminer l'appel" + ToolTip.text: qsTr("call_action_end_call") + Accessible.name: qsTr("call_action_end_call") + Layout.preferredWidth: Utils.getSizeWithScreenRatio(75) + Layout.preferredHeight: Utils.getSizeWithScreenRatio(55) + radius: Utils.getSizeWithScreenRatio(71) + style: ButtonStyle.phoneRedLightBorder + Layout.column: mainWindow.startingCall ? 0 : bottomButtonsLayout.columns - 1 + KeyNavigation.tab: mainWindow.startingCall ? (acceptCallButton.visible ? acceptCallButton : videoCameraButton.visible && videoCameraButton.enabled ? videoCameraButton : audioMicrophoneButton) : openStatisticPanelButton + KeyNavigation.backtab: mainWindow.startingCall ? rightPanel.visible ? Utils.getLastFocusableItemInItem(rightPanel) : nextItemInFocusChain(false): callListButton + onClicked: { + mainWindow.callTerminatedByUser = true + mainWindow.endCall(mainWindow.call) + } } } + // ----------------------------------------------------------------------------- // Group button: pauseCall, transfertCall, newCall, callList // ----------------------------------------------------------------------------- diff --git a/Linphone/view/Style/buttonStyle.js b/Linphone/view/Style/buttonStyle.js index eda1dc96b..c83d6a8c0 100644 --- a/Linphone/view/Style/buttonStyle.js +++ b/Linphone/view/Style/buttonStyle.js @@ -137,6 +137,11 @@ pressed: Linphone.DefaultStyle.grey_0 } } + var phoneGreenLightBorder = Object.assign({ + borderColor : { + keybaordFocused: Linphone.DefaultStyle.main2_0 + } + }, phoneGreen) // Checkable var checkable = { @@ -395,4 +400,4 @@ hovered: Linphone.DefaultStyle.main2_100, pressed: Linphone.DefaultStyle.main2_100 } - } \ No newline at end of file + }