Windows system notifications

This commit is contained in:
gaelle 2026-03-31 14:55:38 +02:00
parent a6974b9e90
commit 57c1a82546
23 changed files with 1306 additions and 181 deletions

View file

@ -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 "$<TARGET_PDB_FILE:${T}>" DESTINATION ${CMAKE_INSTALL_BINDIR})
endforeach ()
endif()
endif()
endif()

View file

@ -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] {

View file

@ -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
};

View file

@ -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()

View file

@ -123,6 +123,7 @@ CallCore::CallCore(const std::shared_ptr<linphone::Call> &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;
}

View file

@ -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;

View file

@ -27,12 +27,8 @@ bool LockEventFilter::nativeEventFilter(const QByteArray &eventType, void *messa
#ifdef Q_OS_WIN
MSG *msg = static_cast<MSG *>(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

View file

@ -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

View file

@ -0,0 +1,10 @@
#include "AbstractNotificationBackend.hpp"
DEFINE_ABSTRACT_OBJECT(AbstractNotificationBackend)
const QHash<int, AbstractNotificationBackend::Notification> AbstractNotificationBackend::Notifications = {
{AbstractNotificationBackend::ReceivedMessage, Notification(AbstractNotificationBackend::ReceivedMessage, 10)},
{AbstractNotificationBackend::ReceivedCall, Notification(AbstractNotificationBackend::ReceivedCall, 30)}};
AbstractNotificationBackend::AbstractNotificationBackend(QObject *parent) : QObject(parent) {
}

View file

@ -0,0 +1,60 @@
#ifndef ABSTRACTNOTIFICATIONBACKEND_HPP
#define ABSTRACTNOTIFICATIONBACKEND_HPP
#include "tool/AbstractObject.hpp"
#include <QHash>
#include <QObject>
#include <QString>
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<ToastButton> &actions = {}) = 0;
virtual void sendNotification(NotificationType type, QVariantMap data) = 0;
static const QHash<int, Notification> Notifications;
signals:
void toastActivated(const QString &args);
private:
DECLARE_ABSTRACT_OBJECT
};
#endif // ABSTRACTNOTIFICATIONBACKEND_HPP

View file

@ -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 <shlobj.h>
#include "DesktopNotificationManagerCompat.hpp"
#include "NotificationActivator.hpp"
#include <NotificationActivationCallback.h>
#include <windows.ui.notifications.h>
#include <appmodel.h>
#include <wrl\wrappers\corewrappers.h>
#include <propkey.h>
#include <propvarutil.h>
#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<IShellLinkW> 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<IPropertyStore> 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<IPersistFile> 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<const BYTE *>(displayName),
static_cast<DWORD>((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<OutOfProc> 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<OutOfProc>::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<OutOfProc>::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<OutOfProc>::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<DWORD>((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<const BYTE *>(exePathStr.c_str()), dataSize));
}
HRESULT CreateToastNotifier(IToastNotifier **notifier) {
RETURN_IF_FAILED(EnsureRegistered());
ComPtr<IToastNotificationManagerStatics> 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<IXmlDocument> answer;
RETURN_IF_FAILED(Windows::Foundation::ActivateInstance(
HStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument).Get(), &answer));
ComPtr<IXmlDocumentIO> 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<IToastNotificationFactory> 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<DesktopNotificationHistoryCompat> *history) {
RETURN_IF_FAILED(EnsureRegistered());
ComPtr<IToastNotificationManagerStatics> toastStatics;
RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory(
HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), &toastStatics));
ComPtr<IToastNotificationManagerStatics2> toastStatics2;
RETURN_IF_FAILED(toastStatics.As(&toastStatics2));
ComPtr<IToastNotificationHistory> nativeHistory;
RETURN_IF_FAILED(toastStatics2->get_History(&nativeHistory));
*history = std::unique_ptr<DesktopNotificationHistoryCompat>(
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<IToastNotificationHistory> 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<ToastNotification *> **toasts) {
ComPtr<IToastNotificationHistory2> 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());
}
}

View file

@ -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 <Windows.h>
#include <memory>
#include <string>
#include <windows.ui.notifications.h>
#include <wrl.h>
#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);
/// <summary>
/// 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.
/// </summary>
/// <param name="aumid">An AUMID that uniquely identifies your application.</param>
/// <param name="clsid">The CLSID of your NotificationActivator class.</param>
HRESULT RegisterAumidAndComServer(const wchar_t *aumid, GUID clsid);
/// <summary>
/// Registers your module to handle COM activations. Call this upon application startup.
/// </summary>
HRESULT RegisterActivator();
/// <summary>
/// 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.
/// </summary>
HRESULT CreateToastNotifier(IToastNotifier **notifier);
/// <summary>
/// Creates an XmlDocument initialized with the specified string. This is simply a convenience helper method.
/// </summary>
HRESULT CreateXmlDocumentFromString(const wchar_t *xmlString, ABI::Windows::Data::Xml::Dom::IXmlDocument **doc);
/// <summary>
/// Creates a toast notification. This is simply a convenience helper method.
/// </summary>
HRESULT CreateToastNotification(ABI::Windows::Data::Xml::Dom::IXmlDocument *content, IToastNotification **notification);
/// <summary>
/// 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.
/// </summary>
HRESULT get_History(std::unique_ptr<DesktopNotificationHistoryCompat> *history);
/// <summary>
/// Gets a boolean representing whether http images can be used within toasts. This is true if running under Desktop
/// Bridge.
/// </summary>
bool CanUseHttpImages();
} // namespace DesktopNotificationManagerCompat
class DesktopNotificationHistoryCompat {
public:
/// <summary>
/// Removes all notifications sent by this app from action center.
/// </summary>
HRESULT Clear();
/// <summary>
/// Gets all notifications sent by this app that are currently still in Action Center.
/// </summary>
HRESULT GetHistory(ABI::Windows::Foundation::Collections::IVectorView<ToastNotification *> **history);
/// <summary>
/// Removes an individual toast, with the specified tag label, from action center.
/// </summary>
/// <param name="tag">The tag label of the toast notification to be removed.</param>
HRESULT Remove(const wchar_t *tag);
/// <summary>
/// Removes a toast notification from the action using the notification's tag and group labels.
/// </summary>
/// <param name="tag">The tag label of the toast notification to be removed.</param>
/// <param name="group">The group label of the toast notification to be removed.</param>
HRESULT RemoveGroupedTag(const wchar_t *tag, const wchar_t *group);
/// <summary>
/// Removes a group of toast notifications, identified by the specified group label, from action center.
/// </summary>
/// <param name="group">The group label of the toast notifications to be removed.</param>
HRESULT RemoveGroup(const wchar_t *group);
/// <summary>
/// Do not call this. Instead, call DesktopNotificationManagerCompat.get_History() to obtain an instance.
/// </summary>
DesktopNotificationHistoryCompat(const wchar_t *aumid, Microsoft::WRL::ComPtr<IToastNotificationHistory> history);
private:
std::wstring m_aumid;
Microsoft::WRL::ComPtr<IToastNotificationHistory> m_history = nullptr;
};

View file

@ -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);

View file

@ -0,0 +1,42 @@
#ifndef NOTIFICATIONACTIVATOR_HPP
#define NOTIFICATIONACTIVATOR_HPP
#pragma once
#include <NotificationActivationCallback.h>
#include <QApplication>
#include <QDebug>
#include <QTimer>
#include <windows.h>
#include <wrl/implements.h>
#include <wrl/module.h>
// #include <wil/com.h>
#include <windows.data.xml.dom.h>
#include <windows.ui.notifications.h>
// #include <winrt/base.h>
// 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<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
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

View file

@ -30,15 +30,13 @@
#include <QTimer>
#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<int, Notifier::Notification> 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<int, Notifier::Notification> 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<QScreen *> 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<QScreen *> 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<QQuickWindow *>(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::ConnectionType>(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<QQuickWindow *>(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::ConnectionType>(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<linphone::Call> &call) {
if (account) {
auto accountModel = Utils::makeQObject_ptr<AccountModel>(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<linphone::Call> &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<linphone::ChatRoom>
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<linphone::ChatRoom>
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)
});
}
}

View file

@ -30,6 +30,8 @@
#include <linphone++/linphone.hh>
// =============================================================================
#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<linphone::Call> &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<QString, int> mScreenHeightOffset;

View file

@ -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 <windows.foundation.h>
#include <windows.ui.notifications.h>
#endif
#include <QDebug>
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 &notif : mPendingNotifications) {
sendNotification(notif.type, notif.data);
}
mPendingNotifications.clear();
}
void NotificationBackend::sendNotification(const QString &title,
const QString &message,
const QList<ToastButton> &actions) {
IToastNotifier *notifier = nullptr;
HRESULT hr = DesktopNotificationManagerCompat::CreateToastNotifier(&notifier);
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"<actions>";
for (const auto &action : actions) {
std::wstring wLabel = action.label.toStdWString();
std::wstring wArg = action.argument.toStdWString();
wActions += L"<action content=\"" + wLabel + L"\" arguments=\"" + wArg + L"\"/>";
}
wActions += L"</actions>";
}
std::wstring xml = L"<toast>"
L" <visual>"
L" <binding template=\"ToastGeneric\">"
L" <text>" +
wTitle +
L"</text>"
L" <text>" +
wMessage +
L"</text>"
L" </binding>"
L" </visual>" +
wActions + L"</toast>";
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<ITypedEventHandler<ToastNotification *, IInspectable *>>(
[this](IToastNotification *sender, IInspectable *args) -> HRESULT {
qInfo() << "Toast clicked!";
// Cast args en IToastActivatedEventArgs
Microsoft::WRL::ComPtr<IToastActivatedEventArgs> 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(&notifier);
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<ChatGui *>();
std::wstring xml = L"<toast>"
L" <visual>"
L" <binding template=\"ToastGeneric\">"
L" <text><![CDATA[" +
chatRoomName +
L"]]></text>"
L" <text><![CDATA[" +
(isGroup ? remoteAddress : L"") +
L"]]></text>"
L" <group>"
L" <subgroup>"
L" <text hint-style=\"body\"><![CDATA[" +
msgTxt +
L"]]></text>"
L" </subgroup>"
L" </group>"
L" </binding>"
L" </visual>"
L" <audio silent=\"true\"/>"
L"</toast>";
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<ITypedEventHandler<ToastNotification *, IInspectable *>>(
[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(&notifier);
if (FAILED(hr) || !notifier) {
lWarning() << "CreateToastNotifier failed:" << Qt::hex << hr;
return;
}
auto displayName = data["displayName"].toString().toStdWString();
CallGui *call = data["call"].value<CallGui *>();
int timeout = 2;
// AbstractNotificationBackend::Notifications[(int)NotificationType::ReceivedCall].getTimeout();
// Incoming call
auto callDescription = tr("incoming_call").toStdWString();
QList<ToastButton> actions;
actions.append(ToastButton(tr("accept_button"), "accept"));
actions.append(ToastButton(tr("decline_button"), "decline"));
std::wstring wActions;
if (!actions.isEmpty()) {
wActions += L"<actions>";
for (const auto &action : actions) {
std::wstring wLabel = action.label.toStdWString();
std::wstring wArg = action.argument.toStdWString();
wActions += L"<action content=\"" + wLabel + L"\" arguments=\"" + wArg + L"\"/>";
}
wActions += L"</actions>";
}
std::wstring xml = L"<toast scenario=\"reminder\">"
L" <visual>"
L" <binding template=\"ToastGeneric\">"
L" <text hint-style=\"header\">" +
displayName +
L"</text>"
L" <text hint-style=\"body\">" +
callDescription +
L"</text>"
L" </binding>"
L" </visual>" +
wActions +
L" <audio silent=\"true\"/>"
L"</toast>";
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<IToastNotification2> 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<const wchar_t *>(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<DesktopNotificationHistoryCompat> history;
DesktopNotificationManagerCompat::get_History(&history);
auto hr = history->RemoveGroupedTag(reinterpret_cast<const wchar_t *>(callId.utf16()), L"linphone");
if (FAILED(hr)) {
qWarning() << "removing toast failed";
}
}
});
EventRegistrationToken token;
toast->add_Activated(Microsoft::WRL::Callback<ITypedEventHandler<ToastNotification *, IInspectable *>>(
[this, call](IToastNotification *sender, IInspectable *args) -> HRESULT {
qInfo() << "Toast clicked!";
// Cast args en IToastActivatedEventArgs
Microsoft::WRL::ComPtr<IToastActivatedEventArgs> 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;
}
}

View file

@ -0,0 +1,41 @@
#ifndef WINDOWSNOTIFICATIONBACKEND_HPP
#define WINDOWSNOTIFICATIONBACKEND_HPP
#include "AbstractNotificationBackend.hpp"
#include <QDebug>
#include <QLocalServer>
#include <QLocalSocket>
#include <QString>
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<ToastButton> &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<PendingNotification> mPendingNotifications;
};
#endif // WINDOWSNOTIFICATIONBACKEND_HPP

View file

@ -1,35 +1,37 @@
#include <QApplication>
#include <QQmlApplicationEngine>
#include <qloggingcategory.h>
#ifdef _WIN32
#include <Windows.h>
FILE *gStream = NULL;
#include <objbase.h> // StringFromCLSID, CoTaskMemFree
#include <propkey.h> // PKEY_AppUserModel_ID, PKEY_AppUserModel_ToastActivatorCLSID
#include <propvarutil.h> // InitPropVariantFromString, PropVariantClear
#include <shlguid.h> // CLSID_ShellLink
#include <shobjidl.h> // 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 <QApplication>
#include <QLocale>
#include <QQmlApplicationEngine>
#include <QSurfaceFormat>
#include <QTranslator>
#include <iostream>
#include <qloggingcategory.h>
#ifdef QT_QML_DEBUG
#include <QQmlDebuggingEnabler>
#include <QStandardPaths>
#endif
#ifdef _WIN32
#include <Windows.h>
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 <QAccessible>
#include <QAccessibleEvent>
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<App>::create(argc, argv);
hr = DesktopNotificationManagerCompat::RegisterActivator();
if (FAILED(hr)) {
qWarning() << "RegisterActivator failed:" << Qt::hex << hr;
}
#ifdef ACCESSBILITY_WORKAROUND
QAccessible::installUpdateHandler(DummyUpdateHandler);
QAccessible::installRootObjectHandler(DummyRootObjectHandler);

View file

@ -40,7 +40,6 @@
// =============================================================================
class AbstractEventCountNotifier;
class EventCountNotifier;
class CoreModel : public ::Listener<linphone::Core, linphone::CoreListener>,

View file

@ -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 {

View file

@ -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
// -----------------------------------------------------------------------------

View file

@ -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
}
}
}