Unity 8
 All Classes Functions Properties
launcherbackend.cpp
1 /*
2  * Copyright (C) 2013 Canonical, Ltd.
3  *
4  * Authors:
5  * Michael Terry <michael.terry@canonical.com>
6  * Michael Zanetti <michael.zanetti@canonical.com>
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; version 3.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "AccountsServiceDBusAdaptor.h"
22 #include "launcherbackend.h"
23 
24 #include <QDir>
25 #include <QDBusArgument>
26 #include <QFileInfo>
27 #include <QGSettings>
28 #include <QDebug>
29 #include <QStandardPaths>
30 
31 class LauncherBackendItem
32 {
33 public:
34  QString desktopFile;
35  QString displayName;
36  QString icon;
37  int count;
38  bool countVisible;
39 };
40 
41 LauncherBackend::LauncherBackend(QObject *parent):
42  QDBusVirtualObject(parent),
43  m_accounts(nullptr)
44 {
45 #ifndef LAUNCHER_TESTING
46  m_accounts = new AccountsServiceDBusAdaptor(this);
47 #endif
48  m_user = qgetenv("USER");
49  syncFromAccounts();
50 
51  /* Set up ourselves on DBus */
52  QDBusConnection con = QDBusConnection::sessionBus();
53  if (!con.registerService("com.canonical.Unity.Launcher")) {
54  qDebug() << "Unable to register launcher name";
55  }
56  if (!con.registerVirtualObject("/com/canonical/Unity/Launcher", this, QDBusConnection::VirtualObjectRegisterOption::SubPath)) {
57  qDebug() << "Unable to register launcher object";
58  }
59 }
60 
61 LauncherBackend::~LauncherBackend()
62 {
63  /* Remove oursevles from DBus */
64  QDBusConnection con = QDBusConnection::sessionBus();
65  con.unregisterService("com.canonical.Unity.Launcher");
66  con.unregisterObject("/com/canonical/Unity/Launcher");
67 
68  /* Clear data */
69  m_storedApps.clear();
70 
71  Q_FOREACH(LauncherBackendItem *item, m_itemCache) {
72  delete item;
73  }
74  m_itemCache.clear();
75 }
76 
77 QStringList LauncherBackend::storedApplications() const
78 {
79  return m_storedApps;
80 }
81 
82 void LauncherBackend::setStoredApplications(const QStringList &appIds)
83 {
84  if (appIds.count() < m_storedApps.count()) {
85  Q_FOREACH(const QString &appId, m_storedApps) {
86  if (!appIds.contains(appId)) {
87  delete m_itemCache.take(appId);
88  }
89  }
90  }
91  m_storedApps = appIds;
92  Q_FOREACH(const QString &appId, appIds) {
93  if (!m_itemCache.contains(appId)) {
94  QString df = findDesktopFile(appId);
95  if (!df.isEmpty()) {
96  LauncherBackendItem *item = parseDesktopFile(df);
97  m_itemCache.insert(appId, item);
98  } else {
99  // Cannot find any data for that app... ignoring it.
100  qWarning() << "cannot find desktop file for" << appId << ". discarding app.";
101  m_storedApps.removeAll(appId);
102  }
103  }
104  }
105  syncToAccounts();
106 }
107 
108 QString LauncherBackend::desktopFile(const QString &appId) const
109 {
110  LauncherBackendItem *item = m_itemCache.value(appId, nullptr);
111  if (item) {
112  return item->desktopFile;
113  }
114 
115  return findDesktopFile(appId);
116 }
117 
118 QString LauncherBackend::displayName(const QString &appId) const
119 {
120  LauncherBackendItem *item = m_itemCache.value(appId, nullptr);
121  if (item) {
122  return item->displayName;
123  }
124 
125  QString df = findDesktopFile(appId);
126  if (!df.isEmpty()) {
127  LauncherBackendItem *item = parseDesktopFile(df);
128  m_itemCache.insert(appId, item);
129  return item->displayName;
130  }
131 
132  return QString();
133 }
134 
135 QString LauncherBackend::icon(const QString &appId) const
136 {
137  QString iconName;
138  LauncherBackendItem *item = getItem(appId);
139  if (item) {
140  iconName = item->icon;
141  }
142 
143  return iconName;
144 }
145 
146 QList<QuickListEntry> LauncherBackend::quickList(const QString &appId) const
147 {
148  // TODO: Get static (from .desktop file) and dynamic (from the app itself)
149  // entries and return them here. Frontend related entries (like "Pin to launcher")
150  // don't matter here. This is just the backend part.
151  // TODO: emit quickListChanged() when the dynamic part changes
152  Q_UNUSED(appId)
153  return QList<QuickListEntry>();
154 }
155 
156 int LauncherBackend::progress(const QString &appId) const
157 {
158  // TODO: Return value for progress emblem.
159  // TODO: emit progressChanged() when this value changes.
160  Q_UNUSED(appId)
161  return -1;
162 }
163 
164 int LauncherBackend::count(const QString &appId) const
165 {
166  int count = -1;
167  LauncherBackendItem *item = getItem(appId);
168 
169  if (item) {
170  if (item->countVisible) {
171  count = item->count;
172  }
173  }
174 
175  return count;
176 }
177 
178 void LauncherBackend::setCount(const QString &appId, int count) const
179 {
180  LauncherBackendItem *item = getItem(appId);
181 
182  bool emitchange = false;
183  if (item) {
184  emitchange = (item->count != count);
185  item->count = count;
186  }
187 
188  if (emitchange) {
189  /* TODO: This needs to use the accessor to handle the visibility
190  correctly, but when we have the two properties we can just use
191  the local value */
192  Q_EMIT countChanged(appId, this->count(appId));
193  QVariant vcount(item->count);
194  emitPropChangedDbus(appId, "count", vcount);
195  }
196 }
197 
198 bool LauncherBackend::countVisible(const QString &appId) const
199 {
200  bool visible = false;
201  LauncherBackendItem *item = getItem(appId);
202 
203  if (item) {
204  visible = item->countVisible;
205  }
206 
207  return visible;
208 }
209 
210 void LauncherBackend::setCountVisible(const QString &appId, bool visible) const
211 {
212  LauncherBackendItem *item = getItem(appId);
213 
214  bool emitchange = false;
215  if (item) {
216  emitchange = (item->countVisible != visible);
217  item->countVisible = visible;
218  } else {
219  qDebug() << "Unable to find:" << appId;
220  }
221 
222  if (emitchange) {
223  /* TODO: Because we're using visible in determining the
224  count we need to emit a count changed as well */
225  Q_EMIT countChanged(appId, this->count(appId));
226  Q_EMIT countVisibleChanged(appId, item->countVisible);
227  QVariant vCountVisible(item->countVisible);
228  emitPropChangedDbus(appId, "countVisible", vCountVisible);
229  }
230 }
231 
232 void LauncherBackend::setUser(const QString &username)
233 {
234  if (qgetenv("USER") == "lightdm" && m_user != username) {
235  m_user = username;
236  syncFromAccounts();
237  }
238 }
239 
240 void LauncherBackend::triggerQuickListAction(const QString &appId, const QString &quickListId)
241 {
242  // TODO: execute the given quicklist action
243  Q_UNUSED(appId)
244  Q_UNUSED(quickListId)
245 }
246 
247 void LauncherBackend::syncFromAccounts()
248 {
249  QList<QVariantMap> apps;
250  bool defaults = true;
251 
252  m_storedApps.clear();
253 
254  if (m_accounts && !m_user.isEmpty()) {
255  QVariant variant = m_accounts->getUserProperty(m_user, "com.canonical.unity.AccountsService", "launcher-items");
256  if (variant.isValid() && variant.canConvert<QDBusArgument>()) {
257  apps = qdbus_cast<QList<QVariantMap>>(variant.value<QDBusArgument>());
258  defaults = isDefaultsItem(apps);
259  }
260  }
261 
262  if (m_accounts && defaults) { // Checking accounts as it'll be null when !useStorage
263  QGSettings gSettings("com.canonical.Unity.Launcher", "/com/canonical/unity/launcher/");
264  Q_FOREACH(const QString &entry, gSettings.get("favorites").toStringList()) {
265  if (entry.startsWith("application://")) {
266  QString appId = entry;
267  // Transform "application://foobar.desktop" to "foobar"
268  appId.remove("application://");
269  if (appId.endsWith(".desktop")) {
270  appId.chop(8);
271  }
272  QString df = findDesktopFile(appId);
273 
274  if (!df.isEmpty()) {
275  m_storedApps << appId;
276 
277  if (!m_itemCache.contains(appId)) {
278  m_itemCache.insert(appId, parseDesktopFile(df));
279  }
280  }
281  }
282  }
283  } else {
284  for (const QVariant &app: apps) {
285  loadFromVariant(app.toMap());
286  }
287  }
288 }
289 
290 void LauncherBackend::syncToAccounts()
291 {
292  if (m_accounts && !m_user.isEmpty()) {
293  QList<QVariantMap> items;
294 
295  Q_FOREACH(const QString &appId, m_storedApps) {
296  items << itemToVariant(appId);
297  }
298 
299  m_accounts->setUserProperty(m_user, "com.canonical.unity.AccountsService", "launcher-items", QVariant::fromValue(items));
300  }
301 }
302 
303 QString LauncherBackend::findDesktopFile(const QString &appId) const
304 {
305  int dashPos = -1;
306  QString helper = appId;
307 
308  QStringList searchDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
309 #ifdef LAUNCHER_TESTING
310  searchDirs << "";
311 #endif
312 
313  do {
314  if (dashPos != -1) {
315  helper = helper.replace(dashPos, 1, '/');
316  }
317 
318  Q_FOREACH(const QString &searchDir, searchDirs) {
319  QFileInfo fileInfo(QDir(searchDir), helper + ".desktop");
320  if (fileInfo.exists()) {
321  return fileInfo.absoluteFilePath();
322  }
323  }
324 
325  dashPos = helper.indexOf("-");
326  } while (dashPos != -1);
327 
328  return QString();
329 }
330 
331 LauncherBackendItem* LauncherBackend::parseDesktopFile(const QString &desktopFile) const
332 {
333  QSettings settings(desktopFile, QSettings::IniFormat);
334 
335  LauncherBackendItem* item = new LauncherBackendItem();
336  item->desktopFile = desktopFile;
337  item->displayName = settings.value("Desktop Entry/Name").toString();
338 
339  QString iconString = settings.value("Desktop Entry/Icon").toString();
340  QString pathString = settings.value("Desktop Entry/Path").toString();
341  if (QFileInfo(iconString).exists()) {
342  item->icon = QFileInfo(iconString).absoluteFilePath();
343  } else if (QFileInfo(pathString + '/' + iconString).exists()) {
344  item->icon = pathString + '/' + iconString;
345  } else {
346  item->icon = "image://theme/" + iconString;
347  }
348 
349  /* TODO: These should be looked up in a cache somewhere */
350  item->count = 0;
351  item->countVisible = false;
352 
353  return item;
354 }
355 
356 /* Gets an item, and tries to create a new one if we need it to */
357 LauncherBackendItem* LauncherBackend::getItem(const QString &appId) const
358 {
359  LauncherBackendItem *item = m_itemCache.value(appId, nullptr);
360  if (!item) {
361  QString df = findDesktopFile(appId);
362  if (!df.isEmpty()) {
363  item = parseDesktopFile(df);
364  if (item) {
365  m_itemCache[appId] = item;
366  } else {
367  qWarning() << "Unable to parse desktop file for" << appId << "path" << df;
368  }
369  } else {
370  qDebug() << "Unable to find desktop file for:" << appId;
371  }
372  }
373 
374  if (!item)
375  qWarning() << "Unable to find item for: " << appId;
376 
377  return item;
378 }
379 
380 void LauncherBackend::loadFromVariant(const QVariantMap &details)
381 {
382  if (!details.contains("id")) {
383  return;
384  }
385  QString appId = details.value("id").toString();
386 
387  LauncherBackendItem *item = m_itemCache.value(appId, nullptr);
388  if (item) {
389  delete item;
390  }
391 
392  item = new LauncherBackendItem();
393 
394  item->desktopFile = details.value("desktopFile").toString();
395  item->displayName = details.value("name").toString();
396  item->icon = details.value("icon").toString();
397  item->count = details.value("count").toInt();
398  item->countVisible = details.value("countVisible").toBool();
399 
400  m_itemCache.insert(appId, item);
401  m_storedApps.append(appId);
402 }
403 
404 QVariantMap LauncherBackend::itemToVariant(const QString &appId) const
405 {
406  LauncherBackendItem *item = m_itemCache.value(appId);
407  QVariantMap details;
408  details.insert("id", appId);
409  details.insert("name", item->displayName);
410  details.insert("icon", item->icon);
411  details.insert("desktopFile", item->desktopFile);
412  details.insert("count", item->count);
413  details.insert("countVisible", item->countVisible);
414  return details;
415 }
416 
417 bool LauncherBackend::isDefaultsItem(const QList<QVariantMap> &apps) const
418 {
419  // To differentiate between an empty list and a list that hasn't been set
420  // yet (and should thus be populated with the defaults), we use a special
421  // list of one item with the 'defaults' field set to true.
422  return (apps.size() == 1 && apps[0].value("defaults").toBool());
423 }
424 
425 bool LauncherBackend::handleMessage(const QDBusMessage& message, const QDBusConnection& connection)
426 {
427  /* Check to make sure we're getting properties on our interface */
428  if (message.type() != QDBusMessage::MessageType::MethodCallMessage) {
429  return false;
430  }
431  if (message.interface() != "org.freedesktop.DBus.Properties") {
432  return false;
433  }
434  if (message.arguments()[0].toString() != "com.canonical.Unity.Launcher.Item") {
435  return false;
436  }
437 
438  /* Break down the path to just the app id */
439  QString pathtemp = message.path();
440  if (!pathtemp.startsWith("/com/canonical/Unity/Launcher/")) {
441  return false;
442  }
443  pathtemp.remove("/com/canonical/Unity/Launcher/");
444  if (pathtemp.indexOf('/') >= 0) {
445  return false;
446  }
447 
448  /* Find ourselves an appid */
449  QString appid = decodeAppId(pathtemp);
450  QVariantList retval;
451 
452  if (message.member() == "Get") {
453  if (message.arguments()[1].toString() == "count") {
454  retval.append(QVariant::fromValue(QDBusVariant(this->count(appid))));
455  } else if (message.arguments()[1].toString() == "countVisible") {
456  retval.append(QVariant::fromValue(QDBusVariant(this->countVisible(appid))));
457  }
458  } else if (message.member() == "Set") {
459  if (message.arguments()[1].toString() == "count") {
460  this->setCount(appid, message.arguments()[2].value<QDBusVariant>().variant().toInt());
461  } else if (message.arguments()[1].toString() == "countVisible") {
462  this->setCountVisible(appid, message.arguments()[2].value<QDBusVariant>().variant().toBool());
463  }
464  } else if (message.member() == "GetAll") {
465  retval.append(this->itemToVariant(appid));
466  } else {
467  return false;
468  }
469 
470  QDBusMessage reply = message.createReply(retval);
471  return connection.send(reply);
472 }
473 
474 QString LauncherBackend::introspect(const QString &path) const
475 {
476  /* This case we should just list the nodes */
477  if (path == "/com/canonical/Unity/Launcher/" || path == "/com/canonical/Unity/Launcher") {
478  QString nodes;
479 
480  Q_FOREACH(const QString &appId, m_itemCache.keys()) {
481  nodes.append("<node name=\"");
482  nodes.append(encodeAppId(appId));
483  nodes.append("\"/>\n");
484  }
485 
486  return nodes;
487  }
488 
489  /* Should not happen, but let's handle it */
490  if (!path.startsWith("/com/canonical/Unity/Launcher")) {
491  return "";
492  }
493 
494  /* Now we should be looking at a node */
495  QString nodeiface =
496  "<interface name=\"com.canonical.Unity.Launcher.Item\">"
497  "<property name=\"count\" type=\"i\" access=\"readwrite\" />"
498  "<property name=\"countVisible\" type=\"b\" access=\"readwrite\" />"
499  "</interface>";
500  return nodeiface;
501 }
502 
503 QString LauncherBackend::decodeAppId(const QString& path)
504 {
505  QByteArray bytes = path.toUtf8();
506  QByteArray decoded;
507 
508  for (int i = 0; i < bytes.size(); ++i) {
509  char chr = bytes.at(i);
510 
511  if (chr == '_') {
512  QString number;
513  number.append(bytes.at(i+1));
514  number.append(bytes.at(i+2));
515 
516  bool okay;
517  char newchar = number.toUInt(&okay, 16);
518  if (okay)
519  decoded.append(newchar);
520 
521  i += 2;
522  } else {
523  decoded.append(chr);
524  }
525  }
526 
527  return QString::fromUtf8(decoded);
528 }
529 
530 QString LauncherBackend::encodeAppId(const QString& appId)
531 {
532  QByteArray bytes = appId.toUtf8();
533  QString encoded;
534 
535  for (int i = 0; i < bytes.size(); ++i) {
536  uchar chr = bytes.at(i);
537 
538  if ((chr >= 'a' && chr <= 'z') ||
539  (chr >= 'A' && chr <= 'Z') ||
540  (chr >= '0' && chr <= '9'&& i != 0)) {
541  encoded.append(chr);
542  } else {
543  QString hexval = QString("_%1").arg(chr, 2, 16, QChar('0'));
544  encoded.append(hexval.toUpper());
545  }
546  }
547 
548  return encoded;
549 }
550 
551 void LauncherBackend::emitPropChangedDbus(const QString& appId, const QString& property, QVariant &value) const
552 {
553  QString path("/com/canonical/Unity/Launcher/");
554  path.append(encodeAppId(appId));
555 
556  QDBusMessage message = QDBusMessage::createSignal(path, "org.freedesktop.DBus.Properties", "PropertiesChanged");
557 
558  QList<QVariant> arguments;
559  QVariantHash changedprops;
560  changedprops[property] = QVariant::fromValue(QDBusVariant(value));
561  QVariantList deletedprops;
562 
563  arguments.append(changedprops);
564  arguments.append(deletedprops);
565 
566  message.setArguments(arguments);
567 
568  QDBusConnection con = QDBusConnection::sessionBus();
569  con.send(message);
570 }