/*
* Copyright (C) 2010 Canonical, Ltd.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* version 3.0 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License version 3.0 for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see
* .
*
* Authored by Mikkel Kamstrup Erlandsen
*
*/
/*
* IMPLEMENTATION NOTE:
* We want the generatedd C API to be nice and not too Vala-ish. We must
* anticipate that place daemons consuming libunity will be written in
* both Vala and C.
*
*/
using Unity.Internal;
namespace Unity {
/* This is a wrapper for a string[]. This is because the Vala compiler
* doesn't work very well with maps with type .
* If we encapsulate the string[] in an object we have ref counting
* and map generics working again */
[Compact]
private class StringArrayWrapper
{
public string[] strings;
public void take_strings (owned string[] str_arr)
{
strings = (owned) str_arr;
}
}
/**
* A singleton class that caches GLib.AppInfo objects.
* Singletons are evil, yes, but this on slightly less
* so because the exposed API is immutable.
*
* To detect when any of the managed AppInfo objects changes, appears,
* or goes away listen for the 'changed' signal.
*/
public class AppInfoManager : Object
{
private static AppInfoManager singleton = null;
private HashTable appinfo_by_id; /* id or path -> AppInfo */
private HashTable monitors; /* parent uri -> monitor */
private HashTable categories_by_id; /* desktop id or path -> xdg cats */
private HashTable keywords_by_id; /* desktop id or path -> X-GNOME-Keywords and X-AppInstall-Keywords */
private HashTable paths_by_id; /* desktop id -> full path to desktop file */
private List timeout_handlers;
private AppInfoManager ()
{
appinfo_by_id = new HashTable (str_hash, str_equal);
categories_by_id = new HashTable (str_hash, str_equal);
keywords_by_id = new HashTable (str_hash, str_equal);
paths_by_id = new HashTable (str_hash, str_equal);
timeout_handlers = new List ();
monitors = new HashTable (str_hash, str_equal);
foreach (string path in IO.get_system_data_dirs())
{
var dir = File.new_for_path (
Path.build_filename (path, "applications"));
try {
var monitor = dir.monitor_directory (FileMonitorFlags.NONE);
monitor.changed.connect (on_dir_changed);
monitors.insert (dir.get_uri(), monitor);
} catch (IOError e) {
warning ("Error setting up directory monitor on '%s': %s",
dir.get_uri (), e.message);
}
}
}
~AppInfoManager ()
{
timeout_handlers.foreach ((id) =>
{
Source.remove (id);
});
}
[Version (deprecated = true, replacement = "AppInfoManager.get_default")]
public static AppInfoManager get_instance ()
{
return AppInfoManager.get_default ();
}
/**
* Get a ref to the singleton AppInfoManager
*/
public static AppInfoManager get_default ()
{
if (AppInfoManager.singleton == null)
AppInfoManager.singleton = new AppInfoManager();
return AppInfoManager.singleton;
}
/**
* Emitted whenever an AppInfo in any of the monitored paths change.
* Note that @new_appinfo may be null in case it has been removed.
*/
public signal void changed (string id, AppInfo? new_appinfo);
/* Whenever something happens to a monitored file,
* we remove it from the cache */
private void on_dir_changed (FileMonitor mon, File file, File? other_file, FileMonitorEvent e)
{
uint timeout_handler = 0;
timeout_handler = Timeout.add_seconds (2, () =>
{
var desktop_id = file.get_basename ();
var path = file.get_path ();
AppInfo? appinfo;
if (appinfo_by_id.remove (desktop_id))
{
appinfo = lookup (desktop_id);
changed (desktop_id, appinfo);
}
if (appinfo_by_id.remove (path))
{
appinfo = lookup (path);
changed (path, appinfo);
}
timeout_handlers.remove (timeout_handler);
return false;
});
timeout_handlers.append (timeout_handler);
}
/**
* Look up an AppInfo given its desktop id or absolute path. The desktop id
* is the base filename of the .desktop file for the application including
* the .desktop extension.
*
* If the AppInfo is not already cached this method will do synchronous
* IO to look it up.
*/
public AppInfo? lookup (string id)
{
/* Check the cache. Note that null is a legal value since it means that
* the files doesn't exist */
if (appinfo_by_id.lookup_extended (id, null, null))
return appinfo_by_id.lookup (id);
/* Look up by path or by desktop id */
AppInfo? appinfo;
KeyFile? keyfile = new KeyFile ();
if (id.has_prefix("/"))
{
paths_by_id.insert (id, id);
try {
keyfile.load_from_file (id, KeyFileFlags.NONE);
} catch (Error e) {
keyfile = null;
if (!(e is IOError.NOT_FOUND || e is KeyFileError.NOT_FOUND))
warning ("Error loading '%s': %s", id, e.message);
}
var dir = File.new_for_path (id).get_parent ();
var dir_uri = dir.get_uri ();
if (monitors.lookup (dir_uri) == null)
{
try {
var monitor = dir.monitor_directory (FileMonitorFlags.NONE);
monitor.changed.connect (on_dir_changed);
monitors.insert (dir_uri, monitor);
Trace.log_object (this, "Monitoring extra app directory: %s", dir_uri);
} catch (IOError ioe) {
warning ("Error setting up extra app directory monitor on '%s': %s",
dir_uri, ioe.message);
}
}
}
else
{
string path = Path.build_filename ("applications", id, null);
string full_path = null;
try {
keyfile.load_from_data_dirs (path, out full_path, KeyFileFlags.NONE);
} catch (Error e) {
keyfile = null;
if (!(e is IOError.NOT_FOUND || e is KeyFileError.NOT_FOUND))
warning ("Error loading '%s': %s", id, e.message);
}
if (full_path != null)
{
var file = File.new_for_path (full_path);
file = file.resolve_relative_path(file.get_path());
paths_by_id.insert(id, file.get_path());
}
else
paths_by_id.insert(id, null);
}
/* If keyfile is null we had an error loading it */
if (keyfile != null)
{
appinfo = new DesktopAppInfo.from_keyfile (keyfile);
register_categories (id, keyfile);
register_keywords (id, keyfile);
}
else
appinfo = null;
/* If we don't find the file, we also cache that fact since we'll store
* a null AppInfo in that case */
appinfo_by_id.insert (id, appinfo);
return appinfo;
}
/**
* Look up XDG categories for for desktop id or file path @id.
* Returns null if id is not found.
* This method will do sync IO if the desktop file for @id is not
* already cached. So if you are living in an async world you must first
* do an async call to lookup_async(id) before calling this method, in which
* case no sync io will be done.
*/
public unowned string[]? get_categories (string id)
{
/* Make sure we have loaded the relevant .desktop file: */
AppInfo? appinfo = lookup (id);
if (appinfo == null)
return null;
unowned StringArrayWrapper result = categories_by_id[id];
return result != null ? result.strings : null;
}
/**
* Look up keywords for for desktop id or file path @id. The keywords will
* be an amalgamation of the X-GNOME-Keywords and X-AppInstall-Keywords
* fields from the .desktopfile.
* Returns null if id is not found.
* This method will do sync IO if the desktop file for @id is not
* already cached. So if you are living in an async world you must first
* do an async call to lookup_async(id) before calling this method, in which
* case no sync io will be done.
*/
public unowned string[]? get_keywords (string id)
{
/* Make sure we have loaded the relevant .desktop file: */
AppInfo? appinfo = lookup (id);
if (appinfo == null)
return null;
unowned StringArrayWrapper result = keywords_by_id[id];
return result != null ? result.strings : null;
}
/**
* Look up the full path to the desktop file for desktop id @id.
* Returns null if @id is not found.
* This method will do sync IO if the desktop file for @id is not
* already cached. So if you are living in an async world you must
* first do an async call to lookup_async(id) before calling this
* method, in which case no sync io will be done.
*/
public string? get_path (string id)
{
AppInfo? appinfo = lookup (id);
if (appinfo == null)
return null;
return paths_by_id.lookup (id);
}
/**
* Look up an AppInfo given its desktop id or absolute path.
* The desktop id is the base filename of the .desktop file for the
* application including the .desktop extension.
*
* If the AppInfo is not already cached this method will do asynchronous
* IO to look it up.
*/
public async AppInfo? lookup_async (string id) throws Error
{
/* Check the cache. Note that null is a legal value since it means that
* the files doesn't exist */
if (appinfo_by_id.lookup_extended (id, null, null))
return appinfo_by_id.lookup (id);
/* Load it async */
size_t data_size;
uint8[] data;
FileInputStream input;
/* Open from path or by desktop id */
if (id.has_prefix ("/"))
{
var f = File.new_for_path (id);
input = yield f.read_async (Priority.DEFAULT, null);
var dir = f.get_parent ();
var dir_uri = dir.get_uri ();
if (monitors.lookup (dir_uri) == null)
{
try {
var monitor = dir.monitor_directory (FileMonitorFlags.NONE);
monitor.changed.connect (on_dir_changed);
monitors.insert (dir_uri, monitor);
Trace.log_object (this, "Monitoring extra app directory: %s", dir_uri);
} catch (IOError ioe) {
warning ("Error setting up extra app directory monitor on '%s': %s",
dir_uri, ioe.message);
}
}
}
else
{
string path = Path.build_filename ("applications", id, null);
input = yield IO.open_from_data_dirs (path);
}
/* If we don't find the file, we also cache that fact by caching a
* null value for that id */
if (input == null)
{
appinfo_by_id.insert (id, null);
return null;
}
try
{
/* Note that we must manually free 'data' */
yield IO.read_stream_async (input,
Priority.LOW, null,
out data, out data_size);
}
catch (Error e)
{
warning ("Error reading '%s': %s", id, e.message);
return null;
}
var keyfile = new KeyFile ();
try
{
keyfile.load_from_data ((string) data, data_size, KeyFileFlags.NONE);
}
catch (Error ee)
{
warning ("Error parsing '%s': %s", id, ee.message);
return null;
}
/* Create the appinfo and cache it */
var appinfo = new DesktopAppInfo.from_keyfile (keyfile);
appinfo_by_id.insert (id, appinfo);
register_categories (id, keyfile);
register_keywords (id, keyfile);
return appinfo;
}
/* Clear all cached AppInfos */
public void clear ()
{
appinfo_by_id.remove_all ();
categories_by_id.remove_all ();
keywords_by_id.remove_all ();
paths_by_id.remove_all ();
// We don't tear down fs monitors... Dunno if we should...
}
private void register_categories (string id, KeyFile keyfile)
{
try {
string[] categories = keyfile.get_string_list ("Desktop Entry",
"Categories");
var wrapper = new StringArrayWrapper ();
wrapper.take_strings ((owned) categories);
categories_by_id[id] = (owned) wrapper;
} catch (KeyFileError eee) {
/* Unknown key or group. This app has no XDG Catories */
}
}
private void register_keywords (string id, KeyFile keyfile)
{
string[] gkeywords;
string[] akeywords;
string[] xdgkeywords;
try {
gkeywords = keyfile.get_locale_string_list ("Desktop Entry",
"X-GNOME-Keywords");
} catch (KeyFileError e) {
/* Unknown key or group. This app has no X-GNOME-Keywords */
gkeywords = new string[0];
}
try {
akeywords = keyfile.get_locale_string_list ("Desktop Entry",
"X-AppInstall-Keywords");
} catch (KeyFileError e) {
/* Unknown key or group. This app has no X-GNOME-Keywords */
akeywords = new string[0];
}
try {
xdgkeywords = keyfile.get_locale_string_list ("Desktop Entry",
"Keywords");
} catch (KeyFileError e) {
/* Unknown key or group. This app has no standard Keywords */
xdgkeywords = new string[0];
}
/* Copy the two keyword types into one 'keyword' array */
string[] keywords = new string[gkeywords.length + akeywords.length + xdgkeywords.length];
for (int i = 0; i < gkeywords.length; i++)
{
keywords[i] = gkeywords[i];
}
for (int i = 0; i < akeywords.length; i++)
{
keywords[gkeywords.length + i] = akeywords[i];
}
for (int i = 0; i < xdgkeywords.length; i++)
{
keywords[gkeywords.length + akeywords.length + i] = xdgkeywords[i];
}
var wrapper = new StringArrayWrapper();
wrapper.take_strings ((owned) keywords);
keywords_by_id[id] = (owned) wrapper;
}
} /* class AppInfoManager */
} /* namespace */