Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

AmberIndicator - A Systray App Under Linux

4.93/5 (12 votes)
27 Sep 2012CPOL5 min read 26.6K   132  
Implemention of an ApplicationIndicator (NotifyIcon under Ubuntu)

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 AppIndicators.

Image 1

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:

C#
  1  namespace AmberIndicator
  2  {
  3      public static class Program
  4      {		
  5          static ApplicationIndicator _indicator;  // this is the "NotifyIcon"
  6          static ImageMenuItem _menuItemShowAlert; // a menu
  7          static Window _dummyForm;                // a hidden mainform	
  8          static string[] _stringsBag;             // magic
  9          static bool _alertRaised;                // whether there's an alert 
 10                                                   //  (an entry in the second feed)
 11          static bool _blinkOn;                    // whether or not the icon is 
 12                                                   //  blinking on/off to indicate 
 13                                                   //  an alert
 14          static System.Timers.Timer _blinkTimer;  // a timer to do said blinking
 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" // AssemblyInfo.Location would point to /usr/bin!
 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.

C#
  1  HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("url to download");
  2  req.IfModifiedSince = // enter day of your local copy
  3  
  4  try
  5  {
  6      var response = req.GetResponse();
  7      string lastModified = response.Headers[HttpResponseHeader.LastModified];
  8  
  9      // since we got this far, we know that the content is outdated.
 10      // a 304 will throw an exception
 11  
 12      // save a local copy of the file.
 13  }
 14  catch (WebException ex)
 15  {
 16      // only handle protocol errors that have valid responses
 17      if (ex.Response == null || ex.Status != WebExceptionStatus.ProtocolError)
 18          throw;
 19  
 20      HttpStatusCode statusCode = ((HttpWebResponse)ex.Response).StatusCode;
 21      if (HttpStatusCode.NotModified == statusCode)  // 304, server sayin' it ain't modified
 22      {
 23          // fetch the old stored version
 24      }
 25  }
 26  // return the content

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.

C#
  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 strings 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 strings 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 shared 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:

Image 2

History

  • 26th September, 2012: version 1.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)