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