Introduction
This article shows how to implement an AppIndicator[^], a kind of SysTray
in Ubuntu, and react to a specific RSS-feed.
Background
There's a free Dutch service called "AMBER Alert[^]", that broadcasts a nation-wide message when a child goes missing. One of the clients they provide is a Windows-app that sits in your system-tray, and notifies the user when there's an alert. It was noted on their website that there wasn't a Linux-version yet, so I tried to puzzle one together. Coming from a Windows-platform, I ran into quite some surprises. Half of this article is dedicated to the installer, as that is where I spent most of the time researching what to do next.
Basically, we're listening to two RSS-feeds, which are nicely documented (in English!) here[^]. The first one is a list of missing children, the second one is reserved for immediate alerts.
The common NotifyIcon
isn't visible if you're running a default KDE-desktop; it requires a Gnome Panel called Notification Area Applet[^]. The image below shows the original NotifyIcon
of the Windows-client running on Ubuntu 12.04, in the lower-left corner (using Wine). Those things on the upper-right corner, are the AppIndicator
s.
Using the Code
C# would be my language of choice. We could use the System.Windows.Forms.NotifyIcon[^], but then we'd still end up in the Gnome panel. Luckily, the manual has a code-example on building an Application Indicators[^] in C# using Gtk#. Another useful example can be found here[^].
Assuming that you've installed Mono and the MonoDevelop[^] IDE, you still need one more additional package. So, open a Terminal-window (the command line interface), and enter the command below:
apt-get install libappindicator0.1-cil-dev libappindicator0.1-cil
When you open the solution, you'll recognize the basic framework of the sample:
1 namespace AmberIndicator
2 {
3 public static class Program
4 {
5 static ApplicationIndicator _indicator;
6 static ImageMenuItem _menuItemShowAlert;
7 static Window _dummyForm;
8 static string[] _stringsBag;
9 static bool _alertRaised;
10
11 static bool _blinkOn;
12
13
14 static System.Timers.Timer _blinkTimer;
15
16 public static void Main ()
17 {
18 Application.Init ();
19
20 _stringsBag = HttpHelpers.DownloadIfModified(
21 AmberQueryThread.QUERY_ROOT + "amberResources.txt")
22 .Split(new string[] { "\n" }, System.StringSplitOptions.RemoveEmptyEntries);
23
24 _dummyForm = new MainWindow();
25 _dummyForm.Visible = false;
26
27 [...]
28
29 _indicator = new ApplicationIndicator
30 (
31 "amber-indicator",
32 "amber16x16x8g",
33 Category.ApplicationStatus,
34 @"/usr/share/amberindicator"
35 );
36 _indicator.Status = Status.Active;
37
38 Menu popupMenu = new Menu ();
39 [...]
40 _indicator.Menu = popupMenu;
41
42 new AmberQueryThread((int missingCount) =>
43 {
44 UpdateAlertStatus(0 != missingCount);
45 });
46
47 Application.Run ();
48 }
We first create a hidden main-window, then the indicator, then we create some menus for the indicator, and finally, we launch a background-thread to poll the RSS-feed.
Points of Interest
IfModifiedSince
We could choose to download the entire thing every now and then, but that would cause a huge amount of traffic. It'd mean that each user would be downloading the same feed over and over. The neat solution is to keep a local copy of the latest version that the server has, and to ask the webserver (from time to time) whether it has an updated version. By setting the IfModifiedSince[^] -property, the server will answer with a status code of 304[^], as opposed to serving the entire file. That way, we can prevent causing a lot of useless traffic.
1 HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("url to download");
2 req.IfModifiedSince =
3
4 try
5 {
6 var response = req.GetResponse();
7 string lastModified = response.Headers[HttpResponseHeader.LastModified];
8
9
10
11
12
13 }
14 catch (WebException ex)
15 {
16
17 if (ex.Response == null || ex.Status != WebExceptionStatus.ProtocolError)
18 throw;
19
20 HttpStatusCode statusCode = ((HttpWebResponse)ex.Response).StatusCode;
21 if (HttpStatusCode.NotModified == statusCode)
22 {
23
24 }
25 }
26
I generalized this construction in the DownloadIfModified
method in the static
class called HttpHelpers
. Which leads us to the next point in the app; where do you keep the 'local' versions? Where do you keep local data at all?
AssemblyDatabase
Decided to re-use a tested method, and to use a Sqlite[^] database to store the data. There's another static
class called AssemblyDatabase
that creates a connection to a database, and that holds the logic to create and initialize a new one. If you look at it, you'll encounter some calls to AssemblyInfo.GetResourceAsString
.
1 internal static string GetResourceAsString(string AResourceName)
2 {
3 string pathAndResourceName = Path.Combine(
4 @"/usr/share/amberindicator",
5 AResourceName);
6 return File.ReadAllText(pathAndResourceName);
7 }
As the sqlite-documentation notes, there's no support for such a thing as a stored procedure. Storing the query as a text-file was the next-best-thing I could think of. They're not traditional resource-files, just silly plain-text files. You'll find all the SQL-commands in the solution, simply copied to the output-directory when updated.
StringsBag?
Traditionally, we keep our string
s in a resource-file. That centralizes things a bit, making for easy maintenance and translation. Since I already had a simple way to download a modified text-file, it seemed like a novel idea to have those string
s available online. You can actually request them here[^]. As you can see, it's a flat text-file. Updating it will cause the clients to download a new version of the file, giving me a way to "update" (part of) the app without having to distribute binaries for the update.
Creating an Installer
We're not finished if there's no installer, and we can't use an MSI-based setup - we'll have to create a so-called "Debian package". There's a helpful tutorial here[^], be sure to also read the "Next" page there. In essence, you rebuild the entire directory-tree where you want to install, starting from the root. The installer will create the below structure:
- /usr/bin/amberindicator.exe
- /usr/share/amberindicator/amber16x16x8.png
- /usr/share/amberindicator/amber16x16x8g.png
- /usr/share/amberindicator/_CreateDb.sql
- /usr/share/amberindicator/_FetchFromDownloadCache.sql
- /usr/share/amberindicator/_InsertIntoDownloadCache.sql
- /usr/share/amberindicator/_SelectLastServerModifiedFromDownloadCacheWhereSource.sql
- /usr/share/amberindicator/_UpdateDownloadCache.sql
- /usr/share/doc/amberindicator/changelog.Debian.gz
- /usr/share/doc/amberindicator/changelog.gz
- /usr/share/doc/amberindicator/copyright
- /usr/share/man/man1/amberindicator.exe.1.gz
- /DEBIAN/control
The bin folder holds the main-executable, while the rest of the app is installed in subdirectory of a share
d folder. That means that the app and its data are split over two directories, something that we don't do on Windows. The rest of the files are support-files for the installer.
/usr/share/doc contains (zipped) versions of the changelog, and a copyright notice. These files must follow a specific format, or the package-builder will present you a list of errors. The changelogs are nearly similar for this particular project. Every application under Linux also has a help-file, called the "man page". You can request the manual from the terminal by entering "man amberindicator
", and it is also a required part of the installer.
/DEBIAN/control
The control file[^] is where you'll set most of the options for the installer. The cool part here are the dependencies:
Package: amberindicator
Version: 1.0.0-1
Section: misc
Priority: optional
Architecture: i386
Depends: debhelper (>= 5), libappindicator0.1-cil-dev (>=0.4.92-0),
libappindicator0.1-cil (>=0.4.92-0), mono-runtime (>=2.10.8.1-1)
Installed-Size: 34
Maintainer: Eddy Vluggen <amberindicator@eddyvluggen.info>
Homepage: http://www.compu-link.net/index.php?id=amber-client-for-linux
Description: Statusindicator for the Dutch AMBER-alert service.
The Dutch AMBER-alert service (http://www.amberalertnederland.nl)
exposes a RSS-feed. Whenever a child is missing and an Alert is
sent out, the feed is updated. The statusindicator queries the
feed in the background, and provides a direct link to the
page with further details.
.
Written under Mono.
</amberindicator@eddyvluggen.info>
To build it, open the terminal-window, change to the root of the directory-structure you created, and enter the commands below:
fakeroot dpkg-deb --build debian
lintian debian.deb
The command lintian
checks whether the installer is "good enough". If it doesn't include everything that lintian wants, you'll get a nasty warning when installing the application.
..finally, when all is done, the results look like this:
History
- 26th September, 2012: version 1.0