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