• Skip to content
  • Skip to link menu
KDE API Documentation - DownloadManager.cpp Source File (GCompris-qt)
  • KDE Home
  • Contact Us
 

GCompris-qt

  • src
  • core
DownloadManager.cpp
1 /* GCompris - DownloadManager.cpp
2  *
3  * Copyright (C) 2014 Holger Kaelberer <holger.k@elberer.de>
4  *
5  * Authors:
6  * Holger Kaelberer <holger.k@elberer.de>
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include "DownloadManager.h"
23 #include "ApplicationSettings.h"
24 #include "ApplicationInfo.h"
25 
26 #include <QFile>
27 #include <QDir>
28 #include <QResource>
29 #include <QStandardPaths>
30 #include <QMutexLocker>
31 #include <QNetworkConfiguration>
32 #include <QDirIterator>
33 
34 const QString DownloadManager::contentsFilename = QString("Contents");
35 DownloadManager* DownloadManager::_instance = 0;
36 
37 /* Public interface: */
38 
39 DownloadManager::DownloadManager()
40  : accessManager(this), serverUrl(ApplicationSettings::getInstance()->downloadServerUrl())
41 {
42  // Cleanup of previous data directory no more used
43  QDir previousDataLocation = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/data";
44  if(previousDataLocation.exists()) {
45  qDebug() << "Remove previous directory data: " << previousDataLocation;
46  previousDataLocation.removeRecursively();
47  }
48 }
49 
50 DownloadManager::~DownloadManager()
51 {
52  shutdown();
53  _instance = 0;
54 }
55 
56 void DownloadManager::shutdown()
57 {
58  qDebug() << "DownloadManager: shutting down," << activeJobs.size() << "active jobs";
59  abortDownloads();
60 }
61 
62 // It is not recommended to create a singleton of Qml Singleton registered
63 // object but we could not found a better way to let us access DownloadManager
64 // on the C++ side. All our test shows that it works.
65 // Using the singleton after the QmlEngine has been destroyed is forbidden!
66 DownloadManager* DownloadManager::getInstance()
67 {
68  if (!_instance)
69  _instance = new DownloadManager;
70  return _instance;
71 }
72 
73 QObject *DownloadManager::systeminfoProvider(QQmlEngine *engine,
74  QJSEngine *scriptEngine)
75 {
76  Q_UNUSED(engine)
77  Q_UNUSED(scriptEngine)
78 
79  return getInstance();
80 }
81 
82 void DownloadManager::init()
83 {
84  qmlRegisterSingletonType<DownloadManager>("GCompris", 1, 0,
85  "DownloadManager",
86  systeminfoProvider);
87 }
88 
89 bool DownloadManager::downloadIsRunning() const
90 {
91  return (activeJobs.size() > 0);
92 }
93 
94 void DownloadManager::abortDownloads()
95 {
96  if (activeJobs.size() > 0) {
97  QMutexLocker locker(&jobsMutex);
98  QMutableListIterator<DownloadJob*> iter(activeJobs);
99  while (iter.hasNext()) {
100  DownloadJob *job = iter.next();
101  if (job->reply) {
102  disconnect(job->reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
103  disconnect(job->reply, SIGNAL(error(QNetworkReply::NetworkError)),
104  this, SLOT(handleError(QNetworkReply::NetworkError)));
105  if (job->reply->isRunning()) {
106  qDebug() << "Aborting download job:" << job->url;
107  job->reply->abort();
108  job->file.close();
109  job->file.remove();
110  }
111  delete job->reply;
112  }
113  iter.remove();
114  }
115  locker.unlock();
116  emit error(QNetworkReply::OperationCanceledError, "Download cancelled by user");
117  }
118 }
119 
120 QString DownloadManager::getVoicesResourceForLocale(const QString& locale) const
121 {
122  return QString("data2/voices-" COMPRESSED_AUDIO "/voices-%1.rcc")
123  .arg(ApplicationInfo::getInstance()->getVoicesLocale(locale));
124 }
125 
126 inline QString DownloadManager::getAbsoluteResourcePath(const QString& path) const
127 {
128  foreach (const QString &base, getSystemResourcePaths()) {
129  if (QFile::exists(base + '/' + path))
130  return QString(base + '/' + path);
131  }
132  return QString();
133 }
134 
135 // @FIXME should support a variable subpath lenght like data2/full.rcc"
136 inline QString DownloadManager::getRelativeResourcePath(const QString& path) const
137 {
138  QStringList parts = path.split('/', QString::SkipEmptyParts);
139  if (parts.size() < 3)
140  return QString();
141  return QString(parts[parts.size()-3] + '/' + parts[parts.size()-2]
142  + '/' + parts[parts.size()-1]);
143 }
144 
145 bool DownloadManager::haveLocalResource(const QString& path) const
146 {
147  return (!getAbsoluteResourcePath(path).isEmpty());
148 }
149 
150 bool DownloadManager::updateResource(const QString& path)
151 {
152  if (checkDownloadRestriction())
153  return downloadResource(path); // check for updates and register
154  else {
155  QString absPath = getAbsoluteResourcePath(path);
156  // automatic download prohibited -> register if available
157  if (!absPath.isEmpty())
158  return registerResource(absPath);
159  else {
160  qDebug() << "No such local resource and download prohibited:"
161  << path;
162  return false;
163  }
164  }
165 }
166 
167 bool DownloadManager::downloadResource(const QString& path)
168 {
169  qDebug() << "Downloading resource file" << path;
170  DownloadJob* job = new DownloadJob(QUrl(serverUrl.toString() + '/' + path));
171 
172  {
173  QMutexLocker locker(&jobsMutex);
174  activeJobs.append(job);
175  }
176 
177  if (!download(job)) {
178  QMutexLocker locker(&jobsMutex);
179  activeJobs.removeOne(job);
180  return false;
181  }
182  return true;
183 }
184 
185 #if 0
186 // vvv might be helpful later with other use-cases:
187 void DownloadManager::registerLocalResources()
188 {
189  QStringList filenames = getLocalResources();
190  if (filenames == QStringList()) {
191  qDebug() << "No local resources found";
192  return;
193  }
194 
195  QList<QString>::const_iterator iter;
196  for (iter = filenames.constBegin(); iter != filenames.constEnd(); iter++)
197  registerResource(*iter);
198 }
199 
200 bool DownloadManager::checkForUpdates()
201 {
202  QStringList filenames = getLocalResources();
203  if (filenames == QStringList()) {
204  qDebug() << "No local resources found";
205  return true;
206  }
207 
208  if (!checkDownloadRestriction()) {
209  qDebug() << "Can't download with current network connection (" <<
210  networkConfiguration.bearerTypeName() << ")!";
211  return false;
212  }
213 
214  QList<QString>::const_iterator iter;
215  DownloadJob *job = new DownloadJob();
216  for (iter = filenames.constBegin(); iter != filenames.constEnd(); iter++) {
217  QUrl url = getUrlForFilename(*iter);
218  qDebug() << "Scheduling resource for update: " << url;
219  job->queue.append(url);
220  }
221  job->url = job->queue.takeFirst();
222 
223  {
224  QMutexLocker locker(&jobsMutex);
225  activeJobs.append(job);
226  }
227 
228  if (!download(job)) {
229  QMutexLocker locker(&jobsMutex);
230  activeJobs.removeOne(job);
231  return false;
232  }
233  return true;
234 
235 }
236 #endif
237 
238 /* Private: */
239 
240 inline QString DownloadManager::tempFilenameForFilename(const QString &filename) const
241 {
242  return QString(filename).append("_");
243 }
244 
245 inline QString DownloadManager::filenameForTempFilename(const QString &tempFilename) const
246 {
247  if (tempFilename.endsWith(QLatin1String("_")))
248  return tempFilename.left(tempFilename.length() - 1);
249  return tempFilename;
250 }
251 
252 bool DownloadManager::download(DownloadJob* job)
253 {
254  QNetworkRequest request;
255 
256  // First download Contents file for verification if not yet done:
257  if (!job->contents.contains(job->url.fileName())) {
258  int len = job->url.fileName().length();
259  QUrl contentsUrl = QUrl(job->url.toString().remove(job->url.toString().length() - len, len)
260  + contentsFilename);
261  if (!job->knownContentsUrls.contains(contentsUrl)) {
262  // Note: need to track already tried Contents files or we can end
263  // up in an infinite loop if corresponding Contents file does not
264  // exist upstream
265  job->knownContentsUrls.append(contentsUrl);
266  //qDebug() << "Postponing rcc download, first fetching Contents" << contentsUrl;
267  job->queue.prepend(job->url);
268  job->url = contentsUrl;
269  }
270  }
271 
272  QFileInfo fi(getFilenameForUrl(job->url));
273  // make sure target path exists:
274  QDir dir;
275  if (!dir.exists(fi.path()) && !dir.mkpath(fi.path())) {
276  qDebug() << "Could not create resource path " << fi.path();
277  emit error(QNetworkReply::ProtocolUnknownError, "Could not create resource path");
278  return false;
279  }
280 
281  job->file.setFileName(tempFilenameForFilename(fi.filePath()));
282  if (!job->file.open(QIODevice::WriteOnly)) {
283  emit error(QNetworkReply::ProtocolUnknownError,
284  QString("Could not open target file %1").arg(job->file.fileName()));
285  return false;
286  }
287 
288  // start download:
289  request.setUrl(job->url);
290  //qDebug() << "Now downloading" << job->url << "to" << fi.filePath() << "...";
291  QNetworkReply *reply = accessManager.get(request);
292  job->reply = reply;
293  connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished()));
294  connect(reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
295  connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
296  this, SLOT(handleError(QNetworkReply::NetworkError)));
297  if (job->url.fileName() != contentsFilename) {
298  connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
299  this, SIGNAL(downloadProgress(qint64,qint64)));
300  emit downloadStarted(job->url.toString().remove(0, serverUrl.toString().length()));
301  }
302 
303  return true;
304 }
305 
306 inline DownloadManager::DownloadJob* DownloadManager::getJobByReply(QNetworkReply *r)
307 {
308  QMutexLocker locker(&jobsMutex);
309  for (int i = 0; i < activeJobs.size(); i++)
310  if (activeJobs[i]->reply == r)
311  return activeJobs[i];
312  return NULL; // should never happen!
313 }
314 
315 void DownloadManager::downloadReadyRead()
316 {
317  QNetworkReply *reply = dynamic_cast<QNetworkReply*>(sender());
318  DownloadJob *job = getJobByReply(reply);
319  job->file.write(reply->readAll());
320 }
321 
322 inline QString DownloadManager::getFilenameForUrl(const QUrl& url) const
323 {
324  QString relPart = url.toString().remove(0, serverUrl.toString().length());
325  return QString(getSystemDownloadPath() + relPart);
326 }
327 
328 inline QUrl DownloadManager::getUrlForFilename(const QString& filename) const
329 {
330  return QUrl(serverUrl.toString() + '/' + getRelativeResourcePath(filename));
331 }
332 
333 inline QString DownloadManager::getSystemDownloadPath() const
334 {
335  return QStandardPaths::writableLocation(QStandardPaths::DataLocation);
336 }
337 
338 inline QStringList DownloadManager::getSystemResourcePaths() const
339 {
340 
341  QStringList results({
342  getSystemDownloadPath(),
343 #if defined(Q_OS_ANDROID)
344  "assets:",
345 #endif
346  QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
347  '/' + GCOMPRIS_APPLICATION_NAME
348  });
349 
350  // Append standard application directories (like /usr/share/applications/)
351  foreach(QString path, QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) {
352  results.append(path + '/' + GCOMPRIS_APPLICATION_NAME);
353  }
354 
355  return results;
356 }
357 
358 bool DownloadManager::checkDownloadRestriction() const
359 {
360 #if 0
361  // note: Something like the following can be used once bearer mgmt
362  // has been implemented for android (cf. Qt bug #30394)
363  QNetworkConfiguration::BearerType conn = networkConfiguration.bearerType();
364  qDebug() << "Bearer type: "<< conn << ": "<< networkConfiguration.bearerTypeName();
365  if (!ApplicationSettings::getInstance()->isMobileNetworkDownloadsEnabled() &&
366  conn != QNetworkConfiguration::BearerEthernet &&
367  conn != QNetworkConfiguration::QNetworkConfiguration::BearerWLAN)
368  return false;
369  return true;
370 #endif
371  return ApplicationSettings::getInstance()->isAutomaticDownloadsEnabled() &&
372  ApplicationInfo::getInstance()->isDownloadAllowed();
373 }
374 
375 void DownloadManager::handleError(QNetworkReply::NetworkError code)
376 {
377  Q_UNUSED(code);
378  QNetworkReply *reply = dynamic_cast<QNetworkReply*>(sender());
379  emit error(reply->error(), reply->errorString());
380 }
381 
382 bool DownloadManager::parseContents(DownloadJob *job)
383 {
384  if (job->file.isOpen())
385  job->file.close();
386 
387  if (!job->file.open(QIODevice::ReadOnly | QIODevice::Text)) {
388  qWarning() << "Could not open file " << job->file.fileName();
389  return false;
390  }
391 
392  /*
393  * We expect the line-syntax, that md5sum and colleagues creates:
394  * <MD5SUM> <FILENAME>
395  * 53f0a3eb206b3028500ca039615c5f84 voices-en.rcc
396  */
397  QTextStream in(&job->file);
398  while (!in.atEnd()) {
399  QString line = in.readLine();
400  QStringList parts = line.split(' ', QString::SkipEmptyParts);
401  if (parts.size() != 2) {
402  qWarning() << "Invalid format of Contents file!";
403  return false;
404  }
405  job->contents[parts[1]] = parts[0];
406  //qDebug() << "Contents: " << parts[1] << " -> " << parts[0];
407  }
408  job->file.close();
409  return true;
410 }
411 
412 bool DownloadManager::checksumMatches(DownloadJob *job, const QString& filename) const
413 {
414  Q_ASSERT(job->contents != (QMap<QString, QString>()));
415 
416  if (!QFile::exists(filename))
417  return false;
418 
419  QString basename = QFileInfo(filename).fileName();
420  if (!job->contents.contains(basename))
421  return false;
422 
423  QFile file(filename);
424  file.open(QIODevice::ReadOnly);
425  QCryptographicHash fileHasher(hashMethod);
426  if (!fileHasher.addData(&file)) {
427  qWarning() << "Could not read file for hashing: " << filename;
428  return false;
429  }
430  file.close();
431  QByteArray fileHash = fileHasher.result().toHex();
432  //qDebug() << "Checking file-hash ~ contents-hash: " << fileHash << " ~ " << job->contents[basename];
433 
434  return (fileHash == job->contents[basename]);
435 }
436 
437 void DownloadManager::unregisterResource_locked(const QString& filename)
438 {
439  if (!QResource::unregisterResource(filename))
440  qDebug() << "Error unregistering resource file" << filename;
441  else {
442  qDebug() << "Successfully unregistered resource file" << filename;
443  registeredResources.removeOne(filename);
444  }
445 }
446 
447 inline bool DownloadManager::isRegistered(const QString& filename) const
448 {
449  return (registeredResources.indexOf(filename) != -1);
450 }
451 
452 bool DownloadManager::registerResource(const QString& filename)
453 {
454  QMutexLocker locker(&rcMutex);
455  if (isRegistered(filename))
456  unregisterResource_locked(filename);
457 
458  if (!QResource::registerResource(filename)) {
459  qDebug() << "Error registering resource file" << filename;
460  return false;
461  } else {
462  qDebug() << "Successfully registered resource"
463  << filename;
464  registeredResources.append(filename);
465 
466  emit resourceRegistered(getRelativeResourcePath(filename));
467 
468  QString v = getVoicesResourceForLocale(
469  ApplicationSettings::getInstance()->locale());
470  if (v == getRelativeResourcePath(filename))
471  emit voicesRegistered();
472  return false;
473  }
474 }
475 
476 bool DownloadManager::isDataRegistered(const QString& data) const
477 {
478  QString res = QString(":/gcompris/data/%1").arg(data);
479 
480  return !QDir(res).entryList().empty();
481 }
482 
483 bool DownloadManager::areVoicesRegistered() const
484 {
485  QString resource = QString("voices-" COMPRESSED_AUDIO "/%1").
486  arg(ApplicationInfo::getInstance()->getVoicesLocale(ApplicationSettings::getInstance()->locale()));
487 
488  return isDataRegistered(resource);
489 }
490 
491 void DownloadManager::downloadFinished()
492 {
493  QNetworkReply* reply = dynamic_cast<QNetworkReply*>(sender());
494  DownloadFinishedCode code = Success;
495  DownloadJob *job = getJobByReply(reply);
496  if (job->file.isOpen()) {
497  job->file.flush(); // note: important, or checksums might be wrong!
498  job->file.close();
499  }
500  if (reply->error() && job->file.exists())
501  job->file.remove();
502  else {
503  // active temp file
504  QString tFilename = filenameForTempFilename(job->file.fileName());
505  if (QFile::exists(tFilename))
506  QFile::remove(tFilename);
507  if (!job->file.rename(tFilename))
508  qWarning() << "Could not rename temporary file to" << tFilename;
509  }
510 
511  QString targetFilename = getFilenameForUrl(job->url);
512  if (job->url.fileName() == contentsFilename) {
513  // Contents
514  if (reply->error()) {
515  qWarning() << "Error downloading Contents from" << job->url
516  << ":" << reply->error() << ":" << reply->errorString();
517  // note: errorHandler() emit's error!
518  code = Error;
519  goto outError;
520  }
521  //qDebug() << "Download of Contents finished successfully: " << job->url;
522  if (!parseContents(job)) {
523  qWarning() << "Invalid format of Contents file" << job->url;
524  emit error(QNetworkReply::UnknownContentError, "Invalid format of Contents file");
525  code = Error;
526  goto outError;
527  }
528  } else {
529  // RCC file
530  if (reply->error()) {
531  qWarning() << "Error downloading RCC file from " << job->url
532  << ":" << reply->error() << ":" << reply->errorString();
533  // note: errorHandler() emit's error!
534  code = Error;
535  // register already existing files:
536  if (QFile::exists(targetFilename))
537  registerResource(targetFilename);
538  } else {
539  qDebug() << "Download of RCC file finished successfully: " << job->url;
540  if (!checksumMatches(job, targetFilename)) {
541  qWarning() << "Checksum of downloaded file does not match: "
542  << targetFilename;
543  emit error(QNetworkReply::UnknownContentError,
544  QString("Checksum of downloaded file does not match: %1")
545  .arg(targetFilename));
546  code = Error;
547  } else
548  registerResource(targetFilename);
549  }
550  }
551 
552  // try next:
553  while (!job->queue.isEmpty()) {
554  job->url = job->queue.takeFirst();
555  QString relPath = getRelativeResourcePath(getFilenameForUrl(job->url));
556  // check in each resource-path for an up2date rcc file:
557  foreach (const QString &base, getSystemResourcePaths()) {
558  QString filename = base + '/' + relPath;
559  if (QFile::exists(filename)
560  && checksumMatches(job, filename))
561  {
562  // file is up2date, register! necessary:
563  qDebug() << "Local resource is up-to-date:"
564  << QFileInfo(filename).fileName();
565  registerResource(filename);
566  code = NoChange;
567  break;
568  }
569  }
570  if (code != NoChange) // nothing is up2date locally -> download it
571  if (download(job))
572  goto outNext;
573  }
574 
575  // none left, DownloadJob finished
576  if (job->file.isOpen())
577  job->file.close();
578  { // note: must remove before signalling downloadFinished(), otherwise race condition for the Qt.quit() case
579  QMutexLocker locker(&jobsMutex);
580  activeJobs.removeOne(job);
581  }
582  emit downloadFinished(code);
583  delete reply;
584  delete job;
585  return;
586 
587  outError:
588  if (job->url.fileName() == contentsFilename) {
589  // if we could not download the contents file register local existing
590  // files for outstanding jobs:
591  QUrl nUrl;
592  while (!job->queue.isEmpty()) {
593  nUrl = job->queue.takeFirst();
594  QString relPath = getRelativeResourcePath(getFilenameForUrl(nUrl));
595  foreach (const QString &base, getSystemResourcePaths()) {
596  QString filename = base + '/' + relPath;
597  if (QFile::exists(filename))
598  registerResource(filename);
599  }
600  }
601  }
602 
603  if (job->file.isOpen())
604  job->file.close();
605 
606  {
607  QMutexLocker locker(&jobsMutex);
608  activeJobs.removeOne(job);
609  }
610 
611  delete reply;
612  delete job;
613  return;
614 
615  outNext:
616  // next sub-job started
617  delete reply;
618  return;
619 }
620 
621 QStringList DownloadManager::getLocalResources()
622 {
623  QStringList result;
624 
625  foreach (const QString &path, getSystemResourcePaths()) {
626  QDir dir(path);
627  if (!dir.exists(path) && !dir.mkpath(path)) {
628  qWarning() << "Could not create resource path " << path;
629  continue;
630  }
631 
632  QDirIterator it(dir, QDirIterator::Subdirectories);
633  while (it.hasNext()) {
634  QString filename = it.next();
635  QFileInfo fi = it.fileInfo();
636  if (fi.isFile() &&
637  (filename.endsWith(QLatin1String(".rcc"))))
638  result.append(filename);
639  }
640  }
641  return result;
642 }
DownloadManager::shutdown
Q_INVOKABLE void shutdown()
Shutdown DownloadManager instance.
Definition: DownloadManager.cpp:56
DownloadManager::abortDownloads
Q_INVOKABLE void abortDownloads()
Abort all currently running downloads.
Definition: DownloadManager.cpp:94
DownloadManager::Success
Download finished successfully.
Definition: DownloadManager.h:212
DownloadManager::downloadStarted
void downloadStarted(const QString &resource)
Emitted when a download has started.
DownloadManager::resourceRegistered
void resourceRegistered(const QString &resource)
Emitted when a resource has been registered.
DownloadManager::NoChange
Local files are up-to-date, no download was needed.
Definition: DownloadManager.h:214
DownloadManager
A singleton class responsible for downloading, updating and maintaining remote resources.
Definition: DownloadManager.h:81
DownloadManager::updateResource
Q_INVOKABLE bool updateResource(const QString &path)
Updates a resource path from the upstream server unless prohibited by the settings and registers it i...
Definition: DownloadManager.cpp:150
DownloadManager::downloadProgress
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
Emitted during a running download.
DownloadManager::downloadResource
Q_INVOKABLE bool downloadResource(const QString &path)
Download a resource specified by the relative resource path and register it if possible.
Definition: DownloadManager.cpp:167
DownloadManager::init
static void init()
Registers DownloadManager singleton in the QML engine.
Definition: DownloadManager.cpp:82
DownloadManager::downloadIsRunning
Q_INVOKABLE bool downloadIsRunning() const
Whether any download is currently running.
Definition: DownloadManager.cpp:89
DownloadManager::error
void error(int code, const QString &msg)
Emitted when a download error occurs.
ApplicationSettings
Singleton that contains GCompris' persistent settings.
Definition: ApplicationSettings.h:63
DownloadManager::isDataRegistered
Q_INVOKABLE bool isDataRegistered(const QString &data) const
Whether the passed relative data is registered.
Definition: DownloadManager.cpp:476
DownloadManager::getVoicesResourceForLocale
Q_INVOKABLE QString getVoicesResourceForLocale(const QString &locale) const
Generates a relative voices resources file-path for a given locale.
Definition: DownloadManager.cpp:120
DownloadManager::voicesRegistered
void voicesRegistered()
Emitted when voices resources for current locale have been registered.
DownloadManager::DownloadFinishedCode
DownloadFinishedCode
Possible return codes of a finished download.
Definition: DownloadManager.h:211
DownloadManager::areVoicesRegistered
Q_INVOKABLE bool areVoicesRegistered() const
Whether voices for the currently active locale are registered.
Definition: DownloadManager.cpp:483
DownloadManager::haveLocalResource
Q_INVOKABLE bool haveLocalResource(const QString &path) const
Checks whether the given relative resource path exists locally.
Definition: DownloadManager.cpp:145
DownloadManager::Error
Download error.
Definition: DownloadManager.h:213
This file is part of the KDE documentation.
Documentation copyright © 1996-2015 The KDE developers.
Generated on Tue Jun 2 2015 21:47:47 by doxygen 1.8.9.1 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

GCompris-qt

Skip menu "GCompris-qt"
  • Main Page
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • File List
  • Modules

Class Picker

Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal