dhcpcd-qt: Add support for KDE5 Notifications
[dhcpcd-ui] / src / dhcpcd-qt / dhcpcd-qt.cpp
1 /*
2  * dhcpcd-qt
3  * Copyright 2014-2017 Roy Marples <roy@marples.name>
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24  * SUCH DAMAGE.
25  */
26
27 #include <QCursor>
28 #include <QDebug>
29 #include <QList>
30 #include <QSocketNotifier>
31 #include <QtGui>
32
33 #include <cerrno>
34
35 #include "config.h"
36 #include "dhcpcd-qt.h"
37 #include "dhcpcd-about.h"
38 #include "dhcpcd-preferences.h"
39 #include "dhcpcd-wi.h"
40 #include "dhcpcd-ifmenu.h"
41 #include "dhcpcd-ssidmenu.h"
42
43
44 #if defined(KNOTIFY5)
45 #include <KNotification>
46 #elif defined(KNOTIFY4)
47 #include <knotification.h>
48 #endif
49
50 DhcpcdQt::DhcpcdQt()
51 {
52
53         createActions();
54         createTrayIcon();
55
56         onLine = carrier = false;
57         lastStatus = DHC_UNKNOWN;
58         aniTimer = new QTimer(this);
59         connect(aniTimer, SIGNAL(timeout()), this, SLOT(animate()));
60         notifier = NULL;
61         retryOpenTimer = NULL;
62
63         about = NULL;
64         preferences = NULL;
65
66         wis = new QList<DhcpcdWi *>();
67         ssidMenu = NULL;
68
69         qDebug("%s", "Connecting ...");
70         con = dhcpcd_new();
71         if (con == NULL) {
72                 qCritical("libdhcpcd: %s", strerror(errno));
73                 exit(EXIT_FAILURE);
74                 return;
75         }
76         dhcpcd_set_progname(con, "dhcpcd-qt");
77         dhcpcd_set_status_callback(con, dhcpcd_status_cb, this);
78         dhcpcd_set_if_callback(con, dhcpcd_if_cb, this);
79         dhcpcd_wpa_set_scan_callback(con, dhcpcd_wpa_scan_cb, this);
80         dhcpcd_wpa_set_status_callback(con, dhcpcd_wpa_status_cb, this);
81         tryOpen();
82 }
83
84 DhcpcdQt::~DhcpcdQt()
85 {
86
87         if (ssidMenu) {
88                 ssidMenu->setVisible(false);
89                 ssidMenu->deleteLater();
90         }
91
92         if (con != NULL) {
93                 dhcpcd_close(con);
94                 dhcpcd_free(con);
95         }
96
97         for (auto &wi : *wis)
98                 wi->deleteLater();
99         delete wis;
100 }
101
102 DHCPCD_CONNECTION *DhcpcdQt::getConnection()
103 {
104
105         return con;
106 }
107
108 QList<DhcpcdWi *> *DhcpcdQt::getWis()
109 {
110
111         return wis;
112 }
113
114 const char * DhcpcdQt::signalStrengthIcon(DHCPCD_WI_SCAN *scan)
115 {
116
117         if (scan == NULL)
118                 return "network-wireless-connected-00";
119         if (scan->strength.value > 80)
120                 return "network-wireless-connected-100";
121         if (scan->strength.value > 55)
122                 return "network-wireless-connected-75";
123         if (scan->strength.value > 30)
124                 return "network-wireless-connected-50";
125         if (scan->strength.value > 5)
126                 return "network-wireless-connected-25";
127         return "network-wireless-connected-00";
128 }
129
130 DHCPCD_WI_SCAN * DhcpcdQt::getStrongestSignal()
131 {
132         DHCPCD_WI_SCAN *scan, *scans, *s;
133         DHCPCD_WPA *wpa;
134         DHCPCD_IF *i;
135
136         scan = NULL;
137         for (auto &wi : *wis) {
138                 wpa = wi->getWpa();
139                 i = dhcpcd_wpa_if(wpa);
140                 scans = wi->getScans();
141                 for (s = scans; s; s = s->next) {
142                         if (dhcpcd_wi_associated(i, s) &&
143                             (scan == NULL ||
144                             s->strength.value > scan->strength.value))
145                                 scan = s;
146                 }
147         }
148         return scan;
149 }
150
151 void DhcpcdQt::animate()
152 {
153         const char *icon;
154         DHCPCD_WI_SCAN *scan;
155
156         scan = getStrongestSignal();
157
158         if (onLine) {
159                 if (aniCounter++ > 6) {
160                         aniTimer->stop();
161                         aniCounter = 0;
162                         return;
163                 }
164
165                 if (aniCounter % 2 == 0)
166                         icon = scan ? "network-wireless-connected-00" :
167                             "network-idle";
168                 else
169                         icon = scan ? DhcpcdQt::signalStrengthIcon(scan) :
170                             "network-transmit-receive";
171         } else {
172                 if (scan) {
173                         switch(aniCounter++) {
174                         case 0:
175                                 icon = "network-wireless-connected-00";
176                                 break;
177                         case 1:
178                                 icon = "network-wireless-connected-25";
179                                 break;
180                         case 2:
181                                 icon = "network-wireless-connected-50";
182                                 break;
183                         case 3:
184                                 icon = "network-wireless-connected-75";
185                                 break;
186                         default:
187                                 icon = "network-wireless-connected-100";
188                                 aniCounter = 0;
189                         }
190                 } else {
191                         switch(aniCounter++) {
192                         case 0:
193                                 icon = "network-transmit";
194                                 break;
195                         case 1:
196                                 icon = "network-receive";
197                                 break;
198                         default:
199                                 icon = "network-idle";
200                                 aniCounter = 0;
201                         }
202                 }
203         }
204
205         setIcon("status", icon);
206 }
207
208 void DhcpcdQt::updateOnline(bool showIf)
209 {
210         bool isOn, isCarrier;
211         char *msg;
212         DHCPCD_IF *ifs, *i;
213         QString msgs;
214
215         isOn = isCarrier = false;
216         ifs = dhcpcd_interfaces(con);
217         for (i = ifs; i; i = i->next) {
218                 if (i->type == DHT_LINK) {
219                         if (i->up)
220                                 isCarrier = true;
221                 } else {
222                         if (i->up)
223                                 isOn = true;
224                 }
225                 msg = dhcpcd_if_message(i, NULL);
226                 if (msg) {
227                         if (showIf)
228                                 qDebug("%s", msg);
229                         if (msgs.isEmpty())
230                                 msgs = QString::fromLatin1(msg);
231                         else
232                                 msgs += '\n' + QString::fromLatin1(msg);
233                         free(msg);
234                 } else if (showIf)
235                         qDebug("%s: %s", i->ifname, i->reason);
236         }
237
238         if (onLine != isOn || carrier != isCarrier) {
239                 onLine = isOn;
240                 carrier = isCarrier;
241                 aniTimer->stop();
242                 aniCounter = 0;
243                 if (isOn) {
244                         animate();
245                         aniTimer->start(300);
246                 } else if (isCarrier) {
247                         animate();
248                         aniTimer->start(500);
249                 } else
250                         setIcon("status", "network-offline");
251         }
252
253         trayIcon->setToolTip(msgs);
254 }
255
256 void DhcpcdQt::statusCallback(unsigned int status, const char *status_msg)
257 {
258
259         qDebug("Status changed to %s", status_msg);
260         if (status == DHC_DOWN) {
261                 aniTimer->stop();
262                 aniCounter = 0;
263                 onLine = carrier = false;
264                 setIcon("status", "network-offline");
265                 trayIcon->setToolTip(tr("Not connected to dhcpcd"));
266                 /* Close down everything */
267                 if (notifier) {
268                         notifier->setEnabled(false);
269                         notifier->deleteLater();
270                         notifier = NULL;
271                 }
272                 if (ssidMenu) {
273                         ssidMenu->deleteLater();
274                         ssidMenu = NULL;
275                 }
276                 preferencesAction->setEnabled(false);
277                 if (preferences) {
278                         preferences->deleteLater();
279                         preferences = NULL;
280                 }
281         } else {
282                 bool refresh;
283
284                 if (lastStatus == DHC_UNKNOWN || lastStatus == DHC_DOWN) {
285                         qDebug("Connected to dhcpcd-%s", dhcpcd_version(con));
286                         refresh = true;
287                 } else
288                         refresh = lastStatus == DHC_OPENED ? true : false;
289                 updateOnline(refresh);
290         }
291
292         lastStatus = status;
293
294         if (status == DHC_DOWN) {
295                 if (retryOpenTimer == NULL) {
296                         retryOpenTimer = new QTimer(this);
297                         connect(retryOpenTimer, SIGNAL(timeout()),
298                             this, SLOT(tryOpen()));
299                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
300                 }
301         }
302 }
303
304 void DhcpcdQt::dhcpcd_status_cb(_unused DHCPCD_CONNECTION *con,
305     unsigned int status, const char *status_msg, void *d)
306 {
307         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
308
309         dhcpcdQt->statusCallback(status, status_msg);
310 }
311
312 void DhcpcdQt::ifCallback(DHCPCD_IF *i)
313 {
314         char *msg;
315         bool new_msg;
316
317         if (i->state == DHS_RENEW ||
318             i->state == DHS_STOP || i->state == DHS_STOPPED)
319         {
320                 msg = dhcpcd_if_message(i, &new_msg);
321                 if (msg) {
322                         qDebug("%s", msg);
323                         if (new_msg) {
324                                 QString t = tr("Network Event");
325                                 QString m = msg;
326                                 QString icon;
327
328                                 if (i->up)
329                                         icon = "network-transmit-receive";
330                                 //else
331                                 //      icon = "network-transmit";
332                                 if (!i->up)
333                                         icon = "network-offline";
334                                 notify(t, m, icon);
335                         }
336                         free(msg);
337                 }
338         }
339
340         updateOnline(false);
341
342         if (i->wireless) {
343                 for (auto &wi : *wis) {
344                         DHCPCD_WPA *wpa = wi->getWpa();
345                         if (dhcpcd_wpa_if(wpa) == i) {
346                                 DHCPCD_WI_SCAN *scans;
347
348                                 scans = dhcpcd_wi_scans(i);
349                                 processScans(wi, scans);
350                         }
351                 }
352         }
353 }
354
355 void DhcpcdQt::dhcpcd_if_cb(DHCPCD_IF *i, void *d)
356 {
357         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
358
359         dhcpcdQt->ifCallback(i);
360 }
361
362 DhcpcdWi *DhcpcdQt::findWi(DHCPCD_WPA *wpa)
363 {
364
365         for (auto &wi : *wis) {
366                 if (wi->getWpa() == wpa)
367                         return wi;
368         }
369         return NULL;
370 }
371
372 void DhcpcdQt::processScans(DhcpcdWi *wi, DHCPCD_WI_SCAN *scans)
373 {
374
375         /* Don't spam the user if we're already connected. */
376         if (lastStatus != DHC_CONNECTED) {
377                 QString title = tr("New Access Point");
378                 QString txt;
379                 DHCPCD_WI_SCAN *s1, *s2;
380
381                 for (s1 = scans; s1; s1 = s1->next) {
382                         for (s2 = wi->getScans(); s2; s2 = s2->next) {
383                                 if (strcmp(s1->ssid, s2->ssid) == 0)
384                                         break;
385                         }
386                         if (s2 == NULL) {
387                                 if (!txt.isEmpty()) {
388                                         title = tr("New Access Points");
389                                         txt += '\n';
390                                 }
391                                 txt += s1->ssid;
392                         }
393                 }
394                 if (!txt.isEmpty() &&
395                     (ssidMenu == NULL || !ssidMenu->isVisible()))
396                         notify(title, txt, "network-wireless");
397         }
398
399         if (wi->setScans(scans) && ssidMenu && ssidMenu->isVisible())
400                 ssidMenu->popup(ssidMenuPos);
401 }
402
403 void DhcpcdQt::scanCallback(DHCPCD_WPA *wpa)
404 {
405         DHCPCD_WI_SCAN *scans;
406         int fd = dhcpcd_wpa_get_fd(wpa);
407         DhcpcdWi *wi;
408
409         wi = findWi(wpa);
410         if (fd == -1) {
411                 qCritical("No fd for WPA");
412                 if (wi) {
413                         wis->removeOne(wi);
414                         wi->close();
415                         wi->deleteLater();
416                 }
417                 return;
418         }
419
420         DHCPCD_IF *i = dhcpcd_wpa_if(wpa);
421         if (i == NULL) {
422                 qCritical("No interface for WPA");
423                 if (wi) {
424                         wis->removeOne(wi);
425                         wi->close();
426                         wi->deleteLater();
427                 }
428                 return;
429         }
430
431         qDebug("%s: Received scan results", i->ifname);
432         scans = dhcpcd_wi_scans(i);
433         if (wi == NULL) {
434                 wi = new DhcpcdWi(this, wpa);
435                 if (wi->open()) {
436                         wis->append(wi);
437                         wi->setScans(scans);
438                 } else {
439                         wi->close();
440                         wi->deleteLater();
441                 }
442         } else
443                 processScans(wi, scans);
444
445         if (!aniTimer->isActive()) {
446                 DHCPCD_WI_SCAN *scan;
447                 const char *icon;
448
449                 scan = getStrongestSignal();
450                 if (scan)
451                         icon = DhcpcdQt::signalStrengthIcon(scan);
452                 else if (onLine)
453                         icon = "network-transmit-receive";
454                 else
455                         icon = "network-offline";
456
457                 setIcon("status", icon);
458         }
459 }
460
461 void DhcpcdQt::dhcpcd_wpa_scan_cb(DHCPCD_WPA *wpa, void *d)
462 {
463         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
464
465         dhcpcdQt->scanCallback(wpa);
466 }
467
468 void DhcpcdQt::wpaStatusCallback(DHCPCD_WPA *wpa,
469     unsigned int status, const char *status_msg)
470 {
471         DHCPCD_IF *i;
472
473         i = dhcpcd_wpa_if(wpa);
474         qDebug("%s: WPA status %s", i->ifname, status_msg);
475         if (status == DHC_DOWN) {
476                 DhcpcdWi *wi = findWi(wpa);
477                 if (wi) {
478                         wis->removeOne(wi);
479                         wi->close();
480                         wi->deleteLater();
481                 }
482         }
483 }
484
485 void DhcpcdQt::dhcpcd_wpa_status_cb(DHCPCD_WPA *wpa,
486     unsigned int status, const char *status_msg, void *d)
487 {
488         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
489
490         dhcpcdQt->wpaStatusCallback(wpa, status, status_msg);
491 }
492
493 void DhcpcdQt::tryOpen() {
494         int fd = dhcpcd_open(con, true);
495         static int last_error;
496
497         if (fd == -1) {
498                 if (errno == EACCES || errno == EPERM) {
499                         if ((fd = dhcpcd_open(con, false)) != -1)
500                                 goto unprived;
501                 }
502                 if (errno != last_error) {
503                         last_error = errno;
504                         const char *errt = strerror(errno);
505                         qCritical("dhcpcd_open: %s", errt);
506                         trayIcon->setToolTip(
507                             tr("Error connecting to dhcpcd: %1").arg(errt));
508                 }
509                 if (retryOpenTimer == NULL) {
510                         retryOpenTimer = new QTimer(this);
511                         connect(retryOpenTimer, SIGNAL(timeout()),
512                             this, SLOT(tryOpen()));
513                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
514                 }
515                 return;
516         }
517
518 unprived:
519         /* Start listening to WPA events */
520         dhcpcd_wpa_start(con);
521
522         if (retryOpenTimer) {
523                 retryOpenTimer->stop();
524                 retryOpenTimer->deleteLater();
525                 retryOpenTimer = NULL;
526         }
527
528         notifier = new QSocketNotifier(fd, QSocketNotifier::Read);
529         connect(notifier, SIGNAL(activated(int)), this, SLOT(dispatch()));
530
531         preferencesAction->setEnabled(dhcpcd_privileged(con));
532 }
533
534 void DhcpcdQt::dispatch()
535 {
536
537         dhcpcd_dispatch(con);
538 }
539
540 void DhcpcdQt::notify(const QString &title, const QString &msg,
541     const QString &icon)
542 {
543
544 #if defined(KNOTIFY4) || defined(KNOTIFY5)
545         KNotification *n = new KNotification("event", this);
546         n->setIconName(icon);
547         n->setTitle(title);
548         n->setText(msg);
549         n->sendEvent();
550 #else
551 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
552         const QIcon i = getIcon("status", icon);
553 #else
554         QSystemTrayIcon::MessageIcon i = QSystemTrayIcon::Information;
555
556         if (icon.compare("network-offline") == 0)
557                 i = QSystemTrayIcon::Warning;
558 #endif
559         trayIcon->showMessage(title, msg, i);
560 #endif
561 }
562
563 void DhcpcdQt::closeEvent(QCloseEvent *event)
564 {
565
566         if (trayIcon->isVisible()) {
567                 hide();
568                 event->ignore();
569         }
570 }
571
572 QIcon DhcpcdQt::getIcon(QString category, QString name)
573 {
574         QIcon icon;
575
576         if (QIcon::hasThemeIcon(name))
577                 icon = QIcon::fromTheme(name);
578         else
579                 icon = QIcon(ICONDIR "/hicolor/scalable/" + category + "/" + name + ".svg");
580
581         return icon;
582 }
583
584 void DhcpcdQt::setIcon(QString category, QString name)
585 {
586         QIcon icon = getIcon(category, name);
587
588         trayIcon->setIcon(icon);
589 }
590
591 QIcon DhcpcdQt::icon()
592 {
593
594         return getIcon("status", "network-transmit-receive");
595 }
596
597 void DhcpcdQt::menuDeleted(QMenu *menu)
598 {
599
600         if (ssidMenu == menu)
601                 ssidMenu = NULL;
602 }
603
604 void DhcpcdQt::createSsidMenu()
605 {
606
607         if (ssidMenu) {
608                 ssidMenu->deleteLater();
609                 ssidMenu = NULL;
610         }
611         if (wis->size() == 0)
612                 return;
613
614         ssidMenu = new QMenu(this);
615         if (wis->size() == 1)
616                 wis->first()->createMenu(ssidMenu);
617         else {
618                 for (auto &wi : *wis)
619                         ssidMenu->addMenu(wi->createIfMenu(ssidMenu));
620         }
621         ssidMenuPos = QCursor::pos();
622         ssidMenu->popup(ssidMenuPos);
623 }
624
625 void DhcpcdQt::iconActivated(QSystemTrayIcon::ActivationReason reason)
626 {
627
628         if (reason == QSystemTrayIcon::Trigger)
629                 createSsidMenu();
630 }
631
632 void DhcpcdQt::dialogClosed(QDialog *dialog)
633 {
634
635         if (dialog == about)
636                 about = NULL;
637         else if (dialog == preferences)
638                 preferences = NULL;
639 }
640
641 void DhcpcdQt::showPreferences()
642 {
643
644         if (preferences == NULL) {
645                 preferences = new DhcpcdPreferences(this);
646                 preferences->show();
647         } else
648                 preferences->activateWindow();
649 }
650
651 void DhcpcdQt::showAbout()
652 {
653
654         if (about == NULL) {
655                 about = new DhcpcdAbout(this);
656                 about->show();
657         } else
658                 about->activateWindow();
659 }
660
661 void DhcpcdQt::createActions()
662 {
663
664         preferencesAction = new QAction(tr("&Preferences"), this);
665         preferencesAction->setIcon(QIcon::fromTheme("preferences-system-network"));
666         preferencesAction->setEnabled(false);
667         connect(preferencesAction, SIGNAL(triggered()),
668             this, SLOT(showPreferences()));
669
670         aboutAction = new QAction(tr("&About"), this);
671         aboutAction->setIcon(QIcon::fromTheme("help-about"));
672         connect(aboutAction, SIGNAL(triggered()), this, SLOT(showAbout()));
673
674         quitAction = new QAction(tr("&Quit"), this);
675         quitAction->setIcon(QIcon::fromTheme("application-exit"));
676         connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
677
678 }
679
680 void DhcpcdQt::createTrayIcon()
681 {
682
683         trayIconMenu = new QMenu(this);
684         trayIconMenu->addAction(preferencesAction);
685         trayIconMenu->addSeparator();
686         trayIconMenu->addAction(aboutAction);
687         trayIconMenu->addAction(quitAction);
688
689         trayIcon = new QSystemTrayIcon(this);
690         setIcon("status", "network-offline");
691         trayIcon->setContextMenu(trayIconMenu);
692
693         connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
694             this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
695
696         trayIcon->show();
697 }