File indexing completed on 2024-04-28 16:54:34
0001 /* 0002 SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0005 */ 0006 0007 #include "job_p.h" 0008 0009 #include "debug.h" 0010 0011 #include <QDBusConnection> 0012 #include <QDebug> 0013 0014 #include <KFilePlacesModel> 0015 #include <KLocalizedString> 0016 #include <KShell> 0017 0018 #include <kio/global.h> 0019 0020 #include "jobviewv2adaptor.h" 0021 #include "jobviewv3adaptor.h" 0022 0023 using namespace NotificationManager; 0024 0025 JobPrivate::JobPrivate(uint id, QObject *parent) 0026 : QObject(parent) 0027 , m_id(id) 0028 { 0029 m_showTimer.setSingleShot(true); 0030 connect(&m_showTimer, &QTimer::timeout, this, &JobPrivate::requestShow); 0031 0032 m_objectPath.setPath(QStringLiteral("/org/kde/notificationmanager/jobs/JobView_%1").arg(id)); 0033 0034 // TODO also v1? it's identical to V2 except it doesn't have setError method so supporting it should be easy 0035 new JobViewV2Adaptor(this); 0036 new JobViewV3Adaptor(this); 0037 0038 QDBusConnection::sessionBus().registerObject(m_objectPath.path(), this); 0039 } 0040 0041 JobPrivate::~JobPrivate() = default; 0042 0043 void JobPrivate::requestShow() 0044 { 0045 if (!m_showRequested) { 0046 m_showRequested = true; 0047 Q_EMIT showRequested(); 0048 } 0049 } 0050 0051 QDBusObjectPath JobPrivate::objectPath() const 0052 { 0053 return m_objectPath; 0054 } 0055 0056 std::shared_ptr<KFilePlacesModel> JobPrivate::createPlacesModel() 0057 { 0058 static std::shared_ptr<KFilePlacesModel> s_instance; 0059 if (!s_instance) { 0060 s_instance = std::make_shared<KFilePlacesModel>(); 0061 } 0062 return s_instance; 0063 } 0064 0065 QUrl JobPrivate::localFileOrUrl(const QString &urlString) 0066 { 0067 QUrl url(urlString); 0068 if (url.scheme().isEmpty()) { 0069 url = QUrl::fromLocalFile(urlString); 0070 } 0071 return url; 0072 } 0073 0074 QString JobPrivate::linkify(const QUrl &url, const QString &caption) 0075 { 0076 return QStringLiteral("<a href=\"%1\">%2</a>").arg(url.toString(QUrl::PrettyDecoded), caption.toHtmlEscaped()); 0077 } 0078 0079 QUrl JobPrivate::destUrl() const 0080 { 0081 QUrl url = m_destUrl; 0082 // In case of a single file and no destUrl, try using the second label (most likely "Destination")... 0083 if (!url.isValid() && m_totalFiles == 1) { 0084 url = localFileOrUrl(m_descriptionValue2).adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0085 } 0086 return url; 0087 } 0088 0089 QString JobPrivate::prettyUrl(const QUrl &_url) const 0090 { 0091 QUrl url(_url); 0092 0093 if (!url.isValid()) { 0094 return QString(); 0095 } 0096 0097 if (url.path().endsWith(QLatin1String("/."))) { 0098 url.setPath(url.path().chopped(2)); 0099 } 0100 0101 if (!m_placesModel) { 0102 m_placesModel = createPlacesModel(); 0103 } 0104 0105 // Mimic KUrlNavigator and show a pretty place name, 0106 // for example Documents/foo/bar rather than /home/user/Documents/foo/bar 0107 const QModelIndex closestIdx = m_placesModel->closestItem(url); 0108 if (closestIdx.isValid()) { 0109 const QUrl placeUrl = m_placesModel->url(closestIdx); 0110 0111 QString text = m_placesModel->text(closestIdx); 0112 0113 QString pathInsidePlace = url.path().mid(placeUrl.path().length()); 0114 0115 if (!pathInsidePlace.startsWith(QLatin1Char('/'))) { 0116 pathInsidePlace.prepend(QLatin1Char('/')); 0117 } 0118 0119 if (pathInsidePlace != QLatin1Char('/')) { 0120 // Avoid "500 GiB Internal Hard Drive/foo/bar" when path originates directly from root. 0121 const bool isRoot = placeUrl.isLocalFile() && placeUrl.path() == QLatin1Char('/'); 0122 if (isRoot) { 0123 text = pathInsidePlace; 0124 } else { 0125 text.append(pathInsidePlace); 0126 } 0127 } 0128 0129 return text; 0130 } 0131 0132 if (url.isLocalFile()) { 0133 return KShell::tildeCollapse(url.toLocalFile()); 0134 } 0135 0136 return url.toDisplayString(QUrl::RemoveUserInfo); 0137 } 0138 0139 void JobPrivate::updateHasDetails() 0140 { 0141 // clang-format off 0142 const bool hasDetails = m_totalBytes > 0 0143 || m_totalFiles > 0 0144 || m_totalDirectories > 0 0145 || m_totalItems > 0 0146 || m_processedBytes > 0 0147 || m_processedFiles > 0 0148 || m_processedDirectories > 0 0149 || m_processedItems > 0 0150 || !m_descriptionValue1.isEmpty() 0151 || !m_descriptionValue2.isEmpty() 0152 || m_speed > 0; 0153 // clang-format on 0154 0155 if (m_hasDetails != hasDetails) { 0156 m_hasDetails = hasDetails; 0157 Q_EMIT static_cast<Job *>(parent())->hasDetailsChanged(); 0158 } 0159 } 0160 0161 QString JobPrivate::text() const 0162 { 0163 if (!m_errorText.isEmpty()) { 0164 return m_errorText; 0165 } 0166 0167 if (!m_infoMessage.isEmpty()) { 0168 return m_infoMessage; 0169 } 0170 0171 const QUrl destUrl = this->destUrl(); 0172 const QString prettyDestUrl = prettyUrl(destUrl); 0173 0174 QString destUrlString; 0175 if (!prettyDestUrl.isEmpty()) { 0176 // Turn destination into a clickable hyperlink 0177 destUrlString = linkify(destUrl, prettyDestUrl); 0178 } 0179 0180 if (m_totalFiles > 1) { 0181 if (!destUrlString.isEmpty()) { 0182 if (m_processedFiles > 0 && m_processedFiles <= m_totalFiles) { 0183 return i18ncp("Copying n of m files to locaton", "%2 of %1 file to %3", "%2 of %1 files to %3", m_totalFiles, m_processedFiles, destUrlString); 0184 } 0185 return i18ncp("Copying n files to location", 0186 "%1 file to %2", 0187 "%1 files to %2", 0188 m_processedFiles > 0 ? m_processedFiles : m_totalFiles, 0189 destUrlString); 0190 } 0191 0192 if (m_processedFiles > 0 && m_processedFiles <= m_totalFiles) { 0193 return i18ncp("Copying n of m files", "%2 of %1 file", "%2 of %1 files", m_totalFiles, m_processedFiles); 0194 } 0195 0196 return i18ncp("Copying n files", "%1 file", "%1 files", m_processedFiles > 0 ? m_processedFiles : m_totalFiles); 0197 } else if (m_totalItems > 1) { 0198 // TODO support destUrl text as well (once someone actually uses that) 0199 0200 if (m_processedItems <= m_totalItems) { 0201 return i18ncp("Copying n of m items", "%2 of %1 item", "%2 of %1 items", m_totalItems, m_processedItems); 0202 } 0203 0204 return i18ncp("Copying n items", "%1 item", "%1 items", m_processedItems > 0 ? m_processedItems : m_totalItems); 0205 } else if (m_totalFiles == 1 || m_totalItems == 1) { 0206 const QUrl url = descriptionUrl(); 0207 0208 if (!destUrlString.isEmpty()) { 0209 const QString currentFileName = url.fileName().toHtmlEscaped(); 0210 if (!currentFileName.isEmpty()) { 0211 return i18nc("Copying file to location", "%1 to %2", currentFileName, destUrlString); 0212 } else { 0213 if (m_totalItems) { 0214 return i18ncp("Copying n items to location", "%1 file to %2", "%1 items to %2", m_totalItems, destUrlString); 0215 } else { 0216 return i18ncp("Copying n files to location", "%1 file to %2", "%1 files to %2", m_totalFiles, destUrlString); 0217 } 0218 } 0219 } else if (url.isValid()) { 0220 // If no destination, show full URL instead of just the file name 0221 return linkify(url, prettyUrl(url)); 0222 } else { 0223 if (m_totalItems) { 0224 return i18ncp("Copying n items", "%1 item", "%1 items", m_totalItems); 0225 } else { 0226 return i18ncp("Copying n files", "%1 file", "%1 files", m_totalFiles); 0227 } 0228 } 0229 } else if (m_totalFiles == 0) { 0230 if (!destUrlString.isEmpty()) { 0231 if (m_processedFiles > 0) { 0232 return i18ncp("Copying n files to location", "%1 file to %2", "%1 files to %2", m_processedFiles, destUrlString); 0233 } 0234 return i18nc("Copying unknown amount of files to location", "to %1", destUrlString); 0235 } else if (m_processedFiles > 0) { 0236 return i18ncp("Copying n files", "%1 file", "%1 files", m_processedFiles); 0237 } 0238 } 0239 0240 qCInfo(NOTIFICATIONMANAGER) << "Failed to generate job text for job with following properties:"; 0241 qCInfo(NOTIFICATIONMANAGER).nospace() << " processedFiles = " << m_processedFiles << ", totalFiles = " << m_totalFiles; 0242 qCInfo(NOTIFICATIONMANAGER).nospace() << " processedItems = " << m_processedItems << ", totalItems = " << m_totalItems; 0243 qCInfo(NOTIFICATIONMANAGER).nospace() << " current file name = " << descriptionUrl().fileName(); 0244 qCInfo(NOTIFICATIONMANAGER).nospace() << " destination url = " << destUrl; 0245 qCInfo(NOTIFICATIONMANAGER).nospace() << " label1 = " << m_descriptionLabel1 << ", value1 = " << m_descriptionValue1; 0246 qCInfo(NOTIFICATIONMANAGER).nospace() << " label2 = " << m_descriptionLabel2 << ", value2 = " << m_descriptionValue2; 0247 0248 return QString(); 0249 } 0250 0251 void JobPrivate::delayedShow(std::chrono::milliseconds delay, ShowConditions showConditions) 0252 { 0253 m_showConditions = showConditions; 0254 0255 if (showConditions.testFlag(ShowCondition::OnTimeout)) { 0256 m_showTimer.start(delay); 0257 } 0258 } 0259 0260 void JobPrivate::kill() 0261 { 0262 Q_EMIT cancelRequested(); 0263 0264 // In case the application doesn't respond, remove the job 0265 if (!m_killTimer) { 0266 m_killTimer = new QTimer(this); 0267 m_killTimer->setSingleShot(true); 0268 connect(m_killTimer, &QTimer::timeout, this, [this] { 0269 qCWarning(NOTIFICATIONMANAGER) << "Application" << m_applicationName << "failed to respond to a cancel request in time"; 0270 Job *job = static_cast<Job *>(parent()); 0271 job->setError(KIO::ERR_USER_CANCELED); 0272 job->setState(Notifications::JobStateStopped); 0273 finish(); 0274 }); 0275 } 0276 0277 if (!m_killTimer->isActive()) { 0278 m_killTimer->start(2000); 0279 } 0280 } 0281 0282 QUrl JobPrivate::descriptionUrl() const 0283 { 0284 QUrl url = localFileOrUrl(m_descriptionValue2); 0285 if (!url.isValid()) { 0286 url = localFileOrUrl(m_descriptionValue1); 0287 } 0288 return url; 0289 } 0290 0291 void JobPrivate::finish() 0292 { 0293 // Unregister the dbus service since the client is done with it 0294 QDBusConnection::sessionBus().unregisterObject(m_objectPath.path()); 0295 0296 // When user canceled job or a transient job finished successfully, remove it without notice 0297 if (m_error == KIO::ERR_USER_CANCELED || (!m_error && m_transient)) { 0298 Q_EMIT closed(); 0299 return; 0300 } 0301 0302 if (m_killTimer) { 0303 m_killTimer->stop(); 0304 } 0305 0306 Job *job = static_cast<Job *>(parent()); 0307 // update timestamp 0308 job->resetUpdated(); 0309 // when it was hidden in history, bring it up again 0310 job->setDismissed(false); 0311 } 0312 0313 // JobViewV2 0314 void JobPrivate::terminate(const QString &errorMessage) 0315 { 0316 Job *job = static_cast<Job *>(parent()); 0317 // forward to JobViewV3. In V2 we get a setError before a terminate 0318 // so we want to forward the current error to the V3 call. 0319 terminate(job->error(), errorMessage, {}); 0320 } 0321 0322 void JobPrivate::setSuspended(bool suspended) 0323 { 0324 Job *job = static_cast<Job *>(parent()); 0325 if (suspended) { 0326 job->setState(Notifications::JobStateSuspended); 0327 } else { 0328 job->setState(Notifications::JobStateRunning); 0329 } 0330 } 0331 0332 void JobPrivate::setTotalAmount(quint64 amount, const QString &unit) 0333 { 0334 if (unit == QLatin1String("bytes")) { 0335 updateField(amount, m_totalBytes, &Job::totalBytesChanged); 0336 } else if (unit == QLatin1String("files")) { 0337 updateField(amount, m_totalFiles, &Job::totalFilesChanged); 0338 } else if (unit == QLatin1String("dirs")) { 0339 updateField(amount, m_totalDirectories, &Job::totalDirectoriesChanged); 0340 } else if (unit == QLatin1String("items")) { 0341 updateField(amount, m_totalItems, &Job::totalItemsChanged); 0342 } 0343 updateHasDetails(); 0344 } 0345 0346 void JobPrivate::setProcessedAmount(quint64 amount, const QString &unit) 0347 { 0348 if (unit == QLatin1String("bytes")) { 0349 updateField(amount, m_processedBytes, &Job::processedBytesChanged); 0350 } else if (unit == QLatin1String("files")) { 0351 updateField(amount, m_processedFiles, &Job::processedFilesChanged); 0352 } else if (unit == QLatin1String("dirs")) { 0353 updateField(amount, m_processedDirectories, &Job::processedDirectoriesChanged); 0354 } else if (unit == QLatin1String("items")) { 0355 updateField(amount, m_processedItems, &Job::processedItemsChanged); 0356 } 0357 updateHasDetails(); 0358 } 0359 0360 void JobPrivate::setPercent(uint percent) 0361 { 0362 const int percentage = static_cast<int>(percent); 0363 if (m_percentage != percentage) { 0364 m_percentage = percentage; 0365 Q_EMIT static_cast<Job *>(parent())->percentageChanged(percentage); 0366 } 0367 } 0368 0369 void JobPrivate::setSpeed(quint64 bytesPerSecond) 0370 { 0371 updateField(bytesPerSecond, m_speed, &Job::speedChanged); 0372 updateHasDetails(); 0373 } 0374 0375 // NOTE infoMessage isn't supposed to be the "Copying..." heading but e.g. a "Connecting to server..." status message 0376 // JobViewV1/V2 got that wrong but JobView3 uses "title" and "infoMessage" correctly respectively. 0377 void JobPrivate::setInfoMessage(const QString &infoMessage) 0378 { 0379 updateField(infoMessage, m_summary, &Job::summaryChanged); 0380 } 0381 0382 bool JobPrivate::setDescriptionField(uint number, const QString &name, const QString &value) 0383 { 0384 bool dirty = false; 0385 if (number == 0) { 0386 dirty |= updateField(name, m_descriptionLabel1, &Job::descriptionLabel1Changed); 0387 dirty |= updateField(value, m_descriptionValue1, &Job::descriptionValue1Changed); 0388 } else if (number == 1) { 0389 dirty |= updateField(name, m_descriptionLabel2, &Job::descriptionLabel2Changed); 0390 dirty |= updateField(value, m_descriptionValue2, &Job::descriptionValue2Changed); 0391 } 0392 if (dirty) { 0393 Q_EMIT static_cast<Job *>(parent())->descriptionUrlChanged(); 0394 updateHasDetails(); 0395 } 0396 0397 return false; 0398 } 0399 0400 void JobPrivate::clearDescriptionField(uint number) 0401 { 0402 setDescriptionField(number, QString(), QString()); 0403 } 0404 0405 void JobPrivate::setDestUrl(const QDBusVariant &urlVariant) 0406 { 0407 QUrl destUrl = QUrl(urlVariant.variant().toUrl().adjusted(QUrl::StripTrailingSlash)); // urgh 0408 if (destUrl.scheme().isEmpty()) { 0409 qCInfo(NOTIFICATIONMANAGER) << "Job from" << m_applicationName << "set a destUrl" << destUrl 0410 << "without a scheme (assuming 'file'), this is an application bug!"; 0411 destUrl.setScheme(QStringLiteral("file")); 0412 } 0413 0414 updateField(destUrl, m_destUrl, &Job::destUrlChanged); 0415 } 0416 0417 void JobPrivate::setError(uint errorCode) 0418 { 0419 static_cast<Job *>(parent())->setError(errorCode); 0420 } 0421 0422 // JobViewV3 0423 void JobPrivate::terminate(uint errorCode, const QString &errorMessage, const QVariantMap &hints) 0424 { 0425 Q_UNUSED(hints) // reserved for future extension 0426 0427 Job *job = static_cast<Job *>(parent()); 0428 job->setError(errorCode); 0429 job->setErrorText(errorMessage); 0430 0431 // Request show just before changing state to stopped, so we're not discarded 0432 if (m_showConditions.testFlag(ShowCondition::OnTermination)) { 0433 requestShow(); 0434 } 0435 0436 job->setState(Notifications::JobStateStopped); 0437 finish(); 0438 } 0439 0440 void JobPrivate::update(const QVariantMap &properties) 0441 { 0442 auto end = properties.end(); 0443 0444 auto it = properties.find(QStringLiteral("title")); 0445 if (it != end) { 0446 updateField(it->toString(), m_summary, &Job::summaryChanged); 0447 } 0448 0449 it = properties.find(QStringLiteral("infoMessage")); 0450 if (it != end) { 0451 // InfoMessage is exposed via text()/BodyRole, not via public API, hence no public signal 0452 const QString infoMessage = it->toString(); 0453 if (m_infoMessage != infoMessage) { 0454 m_infoMessage = it->toString(); 0455 Q_EMIT infoMessageChanged(); 0456 } 0457 } 0458 0459 it = properties.find(QStringLiteral("percent")); 0460 if (it != end) { 0461 setPercent(it->toUInt()); 0462 } 0463 0464 it = properties.find(QStringLiteral("destUrl")); 0465 if (it != end) { 0466 const QUrl destUrl = QUrl(it->toUrl().adjusted(QUrl::StripTrailingSlash)); // urgh 0467 updateField(destUrl, m_destUrl, &Job::destUrlChanged); 0468 } 0469 0470 it = properties.find(QStringLiteral("speed")); 0471 if (it != end) { 0472 setSpeed(it->value<qulonglong>()); 0473 } 0474 0475 updateFieldFromProperties(properties, QStringLiteral("processedFiles"), m_processedFiles, &Job::processedFilesChanged); 0476 updateFieldFromProperties(properties, QStringLiteral("processedBytes"), m_processedBytes, &Job::processedBytesChanged); 0477 updateFieldFromProperties(properties, QStringLiteral("processedDirectories"), m_processedDirectories, &Job::processedDirectoriesChanged); 0478 updateFieldFromProperties(properties, QStringLiteral("processedItems"), m_processedItems, &Job::processedItemsChanged); 0479 0480 updateFieldFromProperties(properties, QStringLiteral("totalFiles"), m_totalFiles, &Job::totalFilesChanged); 0481 updateFieldFromProperties(properties, QStringLiteral("totalBytes"), m_totalBytes, &Job::totalBytesChanged); 0482 updateFieldFromProperties(properties, QStringLiteral("totalDirectories"), m_totalDirectories, &Job::totalDirectoriesChanged); 0483 updateFieldFromProperties(properties, QStringLiteral("totalItems"), m_totalItems, &Job::totalItemsChanged); 0484 0485 updateFieldFromProperties(properties, QStringLiteral("descriptionLabel1"), m_descriptionLabel1, &Job::descriptionLabel1Changed); 0486 updateFieldFromProperties(properties, QStringLiteral("descriptionValue1"), m_descriptionValue1, &Job::descriptionValue1Changed); 0487 updateFieldFromProperties(properties, QStringLiteral("descriptionLabel2"), m_descriptionLabel2, &Job::descriptionLabel2Changed); 0488 updateFieldFromProperties(properties, QStringLiteral("descriptionValue2"), m_descriptionValue2, &Job::descriptionValue2Changed); 0489 0490 it = properties.find(QStringLiteral("suspended")); 0491 if (it != end) { 0492 setSuspended(it->toBool()); 0493 } 0494 0495 updateHasDetails(); 0496 0497 if (!m_summary.isEmpty() && m_showConditions.testFlag(ShowCondition::OnSummary)) { 0498 requestShow(); 0499 } 0500 }