linphone-desktop/Linphone/core/notifier/Notifier.cpp
Julien Wadel cd82964b23 Replace postAsync functions by SafeConnection in order to make more robust thread connections and guarantee to have always caller/callee while processing signals.
Introduce ObjectCore, ObjectGui, ObjectModel.
Simulate addAccount signals on adding account (in waiting for SDK callback).
2023-11-22 14:55:38 +01:00

423 lines
17 KiB
C++

/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QFileInfo>
#include <QMutex>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQuickItem>
#include <QQuickView>
#include <QQuickWindow>
#include <QScreen>
#include <QTimer>
#include "Notifier.hpp"
#include "core/App.hpp"
#include "core/call/CallGui.hpp"
#include "tool/LinphoneEnums.hpp"
#include "tool/providers/ImageProvider.hpp"
DEFINE_ABSTRACT_OBJECT(Notifier)
// =============================================================================
using namespace std;
namespace {
constexpr char NotificationsPath[] = "qrc:/Linphone/view/Item/Notification/";
// ---------------------------------------------------------------------------
// Notifications QML properties/methods.
// ---------------------------------------------------------------------------
constexpr char NotificationShowMethodName[] = "open";
constexpr char NotificationPropertyData[] = "notificationData";
constexpr char NotificationPropertyX[] = "popupX";
constexpr char NotificationPropertyY[] = "popupY";
constexpr char NotificationPropertyWindow[] = "__internalWindow";
constexpr char NotificationPropertyTimer[] = "__timer";
// ---------------------------------------------------------------------------
// Arbitrary hardcoded values.
// ---------------------------------------------------------------------------
constexpr int NotificationSpacing = 10;
constexpr int MaxNotificationsNumber = 5;
} // namespace
// =============================================================================
template <class T>
void setProperty(QObject &object, const char *property, const T &value) {
if (!object.setProperty(property, QVariant(value))) {
qWarning() << QStringLiteral("Unable to set property: `%1`.").arg(property);
abort();
}
}
// =============================================================================
// Available notifications.
// =============================================================================
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}},
//{Notifier::NewVersionAvailable, {Notifier::NewVersionAvailable, "NotificationNewVersionAvailable.qml", 30}},
//{Notifier::SnapshotWasTaken, {Notifier::SnapshotWasTaken, "NotificationSnapshotWasTaken.qml", 10}},
//{Notifier::RecordingCompleted, {Notifier::RecordingCompleted, "NotificationRecordingCompleted.qml", 10}}
};
// -----------------------------------------------------------------------------
Notifier::Notifier(QObject *parent) : QObject(parent) {
mustBeInMainThread(getClassName());
const int nComponents = Notifications.size();
mComponents = new QQmlComponent *[nComponents];
QQmlEngine *engine = App::getInstance()->mEngine;
for (const auto &key : Notifications.keys()) {
QQmlComponent *component =
new QQmlComponent(engine, QUrl(NotificationsPath + Notifier::Notifications[key].filename));
if (Q_UNLIKELY(component->isError())) {
qWarning() << QStringLiteral("Errors found in `Notification` component %1:").arg(key)
<< component->errors();
abort();
}
mComponents[key] = component;
}
mMutex = new QMutex();
}
Notifier::~Notifier() {
mustBeInMainThread("~" + getClassName());
delete mMutex;
const int nComponents = Notifications.size();
for (int i = 0; i < nComponents; ++i)
mComponents[i]->deleteLater();
delete[] mComponents;
}
// -----------------------------------------------------------------------------
QObject *Notifier::createNotification(Notifier::NotificationType type, QVariantMap data) {
QQuickItem *wrapperItem = nullptr;
mMutex->lock();
Q_ASSERT(mInstancesNumber <= MaxNotificationsNumber);
if (mInstancesNumber == MaxNotificationsNumber) { // Check existing instances.
qWarning() << QStringLiteral("Unable to create another notification.");
mMutex->unlock();
return nullptr;
}
QList<QScreen *> allScreens = QGuiApplication::screens();
if (allScreens.size() > 0) { // Ensure to have a screen to avoid errors
QQuickItem *previousWrapper = nullptr;
++mInstancesNumber;
bool showAsTool = false;
#ifdef Q_OS_MACOS
for (auto w : QGuiApplication::topLevelWindows()) {
if ((w->windowState() & Qt::WindowFullScreen) == Qt::WindowFullScreen) {
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) {
// Use QQuickView to create a visual root object that is
// independant from current application Window
QScreen *screen = allScreens[i];
// auto engine = App::getInstance()->mEngine;
auto engine = new QQmlApplicationEngine();
engine->addImageProvider(ImageProvider::ProviderId, new ImageProvider());
engine->addImportPath(":/");
// if(showAsTool) window->setProperty("showAsTool",true);
engine->setInitialProperties(data);
// engine->rootContext()->setContextProperty("applicationDirPath",QGuiApplication::applicationDirPath());
// engine->setInitialProperties({{"screenIndex", i}});
//, {"x", screen->geometry().x()}, {"y", screen->geometry().y()}});
const QUrl url(QString(NotificationsPath) + Notifier::Notifications[type].filename);
QObject::connect(
engine, &QQmlApplicationEngine::objectCreated, this,
[this, url, screen, engine](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl) {
qCritical() << "[App] Notifier.qml couldn't be load.";
engine->deleteLater();
exit(-1);
} else {
qWarning() << engine->rootObjects()[0];
auto window = qobject_cast<QQuickWindow *>(obj);
if (window) {
int *screenHeightOffset = &mScreenHeightOffset[screen->name()]; // Access optimization
QRect availableGeometry = screen->availableGeometry();
int heightOffset =
availableGeometry.y() +
(availableGeometry.height() -
window->height()); //*screen->devicePixelRatio(); when using manual scaler
window->setX(availableGeometry.x() +
(availableGeometry.width() -
window->property("width")
.toInt())); //*screen->devicePixelRatio()); when using manual scaler
window->setY(heightOffset - (*screenHeightOffset % heightOffset));
qWarning() << window->geometry();
}
}
},
Qt::QueuedConnection);
engine->load(url);
}
qInfo() << QStringLiteral("Create notifications:") << wrapperItem;
}
mMutex->unlock();
return wrapperItem;
}
// -----------------------------------------------------------------------------
void Notifier::showNotification(QObject *notification, int timeout) {
// Display notification.
QMetaObject::invokeMethod(notification, NotificationShowMethodName, Qt::DirectConnection);
QTimer *timer = new QTimer(notification);
timer->setInterval(timeout);
timer->setSingleShot(true);
notification->setProperty(NotificationPropertyTimer, QVariant::fromValue(timer));
// Destroy it after timeout.
QObject::connect(timer, &QTimer::timeout, this,
[this, notification]() { deleteNotificationOnTimeout(QVariant::fromValue(notification)); });
// Called explicitly (by a click on notification for example)
QObject::connect(notification, SIGNAL(deleteNotification(QVariant)), this, SLOT(deleteNotification(QVariant)));
timer->start();
}
// -----------------------------------------------------------------------------
void Notifier::deleteNotificationOnTimeout(QVariant notification) {
#ifdef Q_OS_MACOS
for (auto w : QGuiApplication::topLevelWindows()) {
if ((w->windowState() & Qt::WindowFullScreen) == Qt::WindowFullScreen) {
w->requestActivate(); // Used to get focus on fullscreens on Mac in order to avoid screen switching.
}
}
#endif
deleteNotification(notification);
}
void Notifier::deleteNotification(QVariant notification) {
mMutex->lock();
QObject *instance = notification.value<QObject *>();
// Notification marked destroyed.
if (instance->property("__valid").isValid()) {
mMutex->unlock();
return;
}
qInfo() << QStringLiteral("Delete notification:") << instance;
instance->setProperty("__valid", true);
instance->property(NotificationPropertyTimer).value<QTimer *>()->stop();
mInstancesNumber--;
Q_ASSERT(mInstancesNumber >= 0);
if (mInstancesNumber == 0) mScreenHeightOffset.clear();
mMutex->unlock();
instance->deleteLater();
}
// =============================================================================
#define CREATE_NOTIFICATION(TYPE, DATA) \
QObject *notification = createNotification(TYPE, DATA); \
if (!notification) return; \
const int timeout = Notifications[TYPE].getTimeout() * 1000; \
showNotification(notification, timeout);
// -----------------------------------------------------------------------------
// Notification functions.
// -----------------------------------------------------------------------------
void Notifier::notifyReceivedCall(const shared_ptr<linphone::Call> &call) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
auto model = CallCore::create(call);
auto gui = new CallGui(model);
gui->moveToThread(App::getInstance()->thread());
App::postCoreAsync([this, gui]() {
mustBeInMainThread(getClassName());
QVariantMap map;
map["call"].setValue(gui);
CREATE_NOTIFICATION(Notifier::ReceivedCall, map)
QObject::connect(
gui->getCore(), &CallCore::statusChanged, notification,
[this, notification](LinphoneEnums::CallStatus status) {
qInfo() << log().arg("Delete notification on call status : %1").arg(LinphoneEnums::toString(status));
deleteNotification(QVariant::fromValue(notification));
});
QObject::connect(gui->getCore(), &CallCore::destroyed, notification,
[this, notification]() { deleteNotification(QVariant::fromValue(notification)); });
});
}
/*
void Notifier::notifyReceivedMessages(const list<shared_ptr<linphone::ChatMessage>> &messages) {
QVariantMap map;
QString txt;
if (messages.size() > 0) {
shared_ptr<linphone::ChatMessage> message = messages.front();
if (messages.size() == 1) {
auto fileContent = message->getFileTransferInformation();
if (!fileContent) {
foreach (auto content, message->getContents()) {
if (content->isText()) txt += content->getUtf8Text().c_str();
}
} else if (fileContent->isVoiceRecording())
//: 'Voice message received!' : message to warn the user in a notofication for voice messages.
txt = tr("newVoiceMessage");
else txt = tr("newFileMessage");
if (txt.isEmpty() && message->hasConferenceInvitationContent())
//: 'Conference invitation received!' : Notification about receiving an invitation to a conference.
txt = tr("newConferenceInvitation");
} else
//: 'New messages received!' Notification that warn the user of new messages.
txt = tr("newChatRoomMessages");
map["message"] = txt;
shared_ptr<linphone::ChatRoom> chatRoom(message->getChatRoom());
map["timelineModel"].setValue(
CoreManager::getInstance()->getTimelineListModel()->getTimeline(chatRoom, true).get());
if (messages.size() == 1) { // Display only sender on mono message.
map["peerAddress"] = Utils::coreStringToAppString(message->getFromAddress()->asStringUriOnly());
map["fullPeerAddress"] = Utils::coreStringToAppString(message->getFromAddress()->asString());
}
map["localAddress"] = Utils::coreStringToAppString(message->getToAddress()->asStringUriOnly());
map["fullLocalAddress"] = Utils::coreStringToAppString(message->getToAddress()->asString());
map["window"].setValue(App::getInstance()->getMainWindow());
CREATE_NOTIFICATION(Notifier::ReceivedMessage, map)
}
}
void Notifier::notifyReceivedReactions(
const QList<QPair<std::shared_ptr<linphone::ChatMessage>, std::shared_ptr<const linphone::ChatMessageReaction>>>
&reactions) {
QVariantMap map;
QString txt;
if (reactions.size() > 0) {
ChatMessageModel *redirection = nullptr;
QPair<shared_ptr<linphone::ChatMessage>, std::shared_ptr<const linphone::ChatMessageReaction>> reaction =
reactions.front();
shared_ptr<linphone::ChatMessage> message = reaction.first;
shared_ptr<linphone::ChatRoom> chatRoom(message->getChatRoom());
auto timelineModel = CoreManager::getInstance()->getTimelineListModel()->getTimeline(chatRoom, true);
map["messageId"] = Utils::coreStringToAppString(message->getMessageId());
if (reactions.size() == 1) {
QString messageTxt;
auto fileContent = message->getFileTransferInformation();
if (!fileContent) {
foreach (auto content, message->getContents()) {
if (content->isText()) messageTxt += content->getUtf8Text().c_str();
}
} else if (fileContent->isVoiceRecording())
//: 'Voice message' : Voice message type that has been reacted.
messageTxt += tr("voiceMessageReact");
else {
QFileInfo file(Utils::coreStringToAppString(fileContent->getFilePath()));
messageTxt += file.fileName();
}
if (messageTxt.isEmpty() && message->hasConferenceInvitationContent())
//: 'Conference invitation' : Conference invitation message type that has been reacted.
messageTxt += tr("conferenceInvitationReact");
//: ''Has reacted by %1 to: %2' : Reaction message. %1=Reaction(emoji), %2=type of message(Voice
//: Message/Conference invitation/ Message text)
txt = tr("reactionMessage").arg(Utils::coreStringToAppString(reaction.second->getBody())).arg(messageTxt);
} else
//: 'New reactions received!' : Notification that warn the user of new reactions.
txt = tr("newReactionsMessages");
map["message"] = txt;
map["timelineModel"].setValue(timelineModel.get());
if (reactions.size() == 1) { // Display only sender on mono message.
map["peerAddress"] = Utils::coreStringToAppString(reaction.second->getFromAddress()->asStringUriOnly());
map["fullPeerAddress"] = Utils::coreStringToAppString(reaction.second->getFromAddress()->asString());
}
map["localAddress"] = Utils::coreStringToAppString(chatRoom->getLocalAddress()->asStringUriOnly());
map["fullLocalAddress"] = Utils::coreStringToAppString(chatRoom->getLocalAddress()->asString());
map["window"].setValue(App::getInstance()->getMainWindow());
CREATE_NOTIFICATION(Notifier::ReceivedMessage, map)
}
}
void Notifier::notifyReceivedFileMessage(const shared_ptr<linphone::ChatMessage> &message,
const shared_ptr<linphone::Content> &content) {
QVariantMap map;
shared_ptr<linphone::ChatRoom> chatRoom(message->getChatRoom());
map["timelineModel"].setValue(
CoreManager::getInstance()->getTimelineListModel()->getTimeline(chatRoom, true).get());
map["fileUri"] = Utils::coreStringToAppString(content->getFilePath());
if (Utils::getImage(map["fileUri"].toString()).isNull()) map["imageUri"] = "";
else map["imageUri"] = map["fileUri"];
map["fileSize"] = quint64(content->getSize() + content->getFileSize());
CREATE_NOTIFICATION(Notifier::ReceivedFileMessage, map)
}
void Notifier::notifyNewVersionAvailable(const QString &version, const QString &url) {
QVariantMap map;
map["message"] = tr("newVersionAvailable").arg(version);
map["url"] = url;
CREATE_NOTIFICATION(Notifier::NewVersionAvailable, map)
}
void Notifier::notifySnapshotWasTaken(const QString &filePath) {
QVariantMap map;
map["filePath"] = filePath;
CREATE_NOTIFICATION(Notifier::SnapshotWasTaken, map)
}
void Notifier::notifyRecordingCompleted(const QString &filePath) {
QVariantMap map;
map["filePath"] = filePath;
CREATE_NOTIFICATION(Notifier::RecordingCompleted, map)
}
*/
#undef SHOW_NOTIFICATION
#undef CREATE_NOTIFICATION