[gnome-bluetooth] settings-widget: Add Obex preferences



commit dae7b63ec00ed191ae61240334e4938f260d55fa
Author: Bastien Nocera <hadess hadess net>
Date:   Mon Jul 27 17:58:00 2015 +0200

    settings-widget: Add Obex preferences
    
    As we're moving ObexPush preferences to the Bluetooth panel,
    and that unpaired devices will need the computer to be visible
    to be able to send files to it, move the code to receive files
    via Obex to the widget itself, so that it can be launched
    when the dialogue is visible, and turned off when the panel
    exits, or the application quits (whether unexpectedly or not).
    
    https://bugzilla.gnome.org/show_bug.cgi?id=740369

 configure.ac                      |    4 +-
 lib/Makefile.am                   |    6 +-
 lib/bluetooth-settings-obexpush.c |  820 +++++++++++++++++++++++++++++++++++++
 lib/bluetooth-settings-obexpush.h |   60 +++
 lib/bluetooth-settings-widget.c   |  133 +++++--
 lib/settings.ui                   |   67 +++-
 lib/test-settings.c               |    2 +-
 po/POTFILES.in                    |    1 +
 8 files changed, 1034 insertions(+), 59 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 69f6583..632acca 100644
--- a/configure.ac
+++ b/configure.ac
@@ -94,7 +94,9 @@ PKG_CHECK_MODULES(LIBGNOMEBT,
        gmodule-2.0
        gio-unix-2.0
        gtk+-3.0 >= $GTK_REQUIRED
-       libudev)
+       libudev
+       libnotify
+       libcanberra-gtk3)
 
 GDBUS_CODEGEN="gdbus-codegen"
 AC_SUBST(GDBUS_CODEGEN)
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 6093925..80ad1ed 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -29,7 +29,8 @@ libgnome_bluetooth_c_sources =                \
        bluetooth-settings-resources.c  \
        bluetooth-settings-row.c        \
        bluetooth-pairing-dialog.c      \
-       pin.c
+       pin.c                           \
+       bluetooth-settings-obexpush.c
 
 libgnome_bluetooth_private_headers =   \
        bluetooth-client-private.h      \
@@ -41,7 +42,8 @@ libgnome_bluetooth_private_headers =  \
        bluetooth-settings-resources.h  \
        bluetooth-settings-row.h        \
        bluetooth-pairing-dialog.h      \
-       pin.h
+       pin.h                           \
+       bluetooth-settings-obexpush.h
 
 # public headers don't need to be listed, are handled by _HEADERS
 libgnome_bluetooth_la_SOURCES =                        \
diff --git a/lib/bluetooth-settings-obexpush.c b/lib/bluetooth-settings-obexpush.c
new file mode 100644
index 0000000..bf207e3
--- /dev/null
+++ b/lib/bluetooth-settings-obexpush.c
@@ -0,0 +1,820 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+
+/*
+ *  Copyright (C) 2004-2008 Red Hat, Inc.
+ *  Copyright (C) 2013 Intel Corporation.
+ *
+ *  Nautilus is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  Nautilus 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ *  Authors: Bastien Nocera <hadess hadess net>
+ *  Authors: Emilio Pozuelo Monfort <emilio pozuelo collabora co uk>
+ *
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include <bluetooth-client.h>
+#include <libnotify/notify.h>
+#include <canberra-gtk.h>
+
+#include "bluetooth-settings-obexpush.h"
+
+#define MANAGER_SERVICE        "org.bluez.obex"
+#define MANAGER_IFACE  "org.bluez.obex.AgentManager1"
+#define MANAGER_PATH   "/org/bluez/obex"
+
+#define AGENT_PATH     "/org/gnome/share/agent"
+#define AGENT_IFACE    "org.bluez.obex.Agent1"
+
+#define TRANSFER_IFACE "org.bluez.obex.Transfer1"
+#define SESSION_IFACE  "org.bluez.obex.Session1"
+
+static GDBusNodeInfo *introspection_data = NULL;
+
+static const gchar introspection_xml[] =
+"<node name='"AGENT_PATH"'>"
+"  <interface name='"AGENT_IFACE"'>"
+"    <method name='Release'>"
+"    </method>"
+"    <method name='Cancel'>"
+"    </method>"
+"    <method name='AuthorizePush'>"
+"      <arg name='transfer' type='o' />"
+"      <arg name='path' type='s' direction='out' />"
+"    </method>"
+"  </interface>"
+"</node>";
+
+G_DEFINE_TYPE(ObexAgent, obex_agent, G_TYPE_OBJECT)
+
+static ObexAgent *agent;
+static BluetoothClient *client;
+
+static void
+on_close_notification (NotifyNotification *notification)
+{
+       g_object_unref (notification);
+}
+
+static void
+notification_launch_action_on_file_cb (NotifyNotification *notification,
+                                      const char *action,
+                                      const char *file_uri)
+{
+       g_assert (action != NULL);
+
+       /* We launch the file viewer for the file */
+       if (g_str_equal (action, "display") != FALSE) {
+               GdkDisplay *display;
+               GAppLaunchContext *ctx;
+               GTimeVal val;
+
+               g_get_current_time (&val);
+
+               display = gdk_display_get_default ();
+               ctx = G_APP_LAUNCH_CONTEXT (gdk_display_get_app_launch_context (display));
+               gdk_app_launch_context_set_timestamp (GDK_APP_LAUNCH_CONTEXT (ctx), val.tv_sec);
+
+               if (g_app_info_launch_default_for_uri (file_uri, ctx, NULL) == FALSE) {
+                       g_warning ("Failed to launch the file viewer\n");
+               }
+
+               g_object_unref (ctx);
+       }
+
+       /* we open the Downloads folder */
+       if (g_str_equal (action, "reveal") != FALSE) {
+               GDBusConnection *connection = agent->connection;
+               GVariantBuilder builder;
+
+               g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+               g_variant_builder_add (&builder, "s", file_uri);
+
+               g_dbus_connection_call (connection,
+                                       "org.freedesktop.FileManager1",
+                                       "/org/freedesktop/FileManager1",
+                                       "org.freedesktop.FileManager1",
+                                       "ShowItems",
+                                       g_variant_new ("(ass)", &builder, ""),
+                                       NULL,
+                                       G_DBUS_CALL_FLAGS_NONE,
+                                       -1,
+                                       NULL,
+                                       NULL,
+                                       NULL);
+
+               g_variant_builder_clear (&builder);
+       }
+
+       notify_notification_close (notification, NULL);
+}
+
+static void
+show_notification (const char *filename)
+{
+       char *file_uri, *notification_text, *display, *mime_type;
+       NotifyNotification *notification;
+       ca_context *ctx;
+       GAppInfo *app;
+
+       file_uri = g_filename_to_uri (filename, NULL, NULL);
+       if (file_uri == NULL) {
+               g_warning ("Could not make a filename from '%s'", filename);
+               return;
+       }
+
+       display = g_filename_display_basename (filename);
+       /* Translators: %s is the name of the filename received */
+       notification_text = g_strdup_printf(_("You received \"%s\" via Bluetooth"), display);
+       g_free (display);
+       notification = notify_notification_new (_("You received a file"),
+                                               notification_text,
+                                               "bluetooth");
+
+       notify_notification_set_timeout (notification, NOTIFY_EXPIRES_DEFAULT);
+       notify_notification_set_hint_string (notification, "desktop-entry", "gnome-bluetooth-panel");
+
+       mime_type = g_content_type_guess (filename, NULL, 0, NULL);
+       app = g_app_info_get_default_for_type (mime_type, FALSE);
+       if (app != NULL) {
+               g_object_unref (app);
+               notify_notification_add_action (notification, "display", _("Open File"),
+                                               (NotifyActionCallback) notification_launch_action_on_file_cb,
+                                               g_strdup (file_uri), (GFreeFunc) g_free);
+       }
+       notify_notification_add_action (notification, "reveal", _("Reveal File"),
+                                       (NotifyActionCallback) notification_launch_action_on_file_cb,
+                                       g_strdup (file_uri), (GFreeFunc) g_free);
+
+       g_free (file_uri);
+       
+       g_signal_connect (G_OBJECT (notification), "closed", G_CALLBACK (on_close_notification), 
notification);
+
+       if (!notify_notification_show (notification, NULL)) {
+               g_warning ("failed to send notification\n");
+       }
+       g_free (notification_text);
+
+       /* Now we do the audio notification */
+       ctx = ca_gtk_context_get ();
+       ca_context_play (ctx, 0,
+                        CA_PROP_EVENT_ID, "complete-download",
+                        CA_PROP_EVENT_DESCRIPTION, _("File reception complete"),
+                        NULL);
+}
+
+static void
+reject_transfer (GDBusMethodInvocation *invocation)
+{
+       const char *filename;
+
+       filename = g_object_get_data (G_OBJECT (invocation), "temp-filename");
+       g_remove (filename);
+
+       g_dbus_method_invocation_return_dbus_error (invocation,
+               "org.bluez.obex.Error.Rejected", "Not Authorized");
+}
+
+static void
+ask_user_transfer_accepted (NotifyNotification *notification,
+                           char *action,
+                           GDBusMethodInvocation *invocation)
+{
+       gchar *file = g_object_get_data (G_OBJECT (invocation), "temp-filename");
+
+       g_debug ("Notification: transfer accepted! accepting transfer");
+
+       g_dbus_method_invocation_return_value (invocation,
+               g_variant_new ("(s)", file));
+}
+
+static void
+ask_user_transfer_rejected (NotifyNotification *notification,
+                           char *action,
+                           GDBusMethodInvocation *invocation)
+{
+       g_debug ("Notification: transfer rejected! rejecting transfer");
+       reject_transfer (invocation);
+}
+
+static void
+ask_user_on_close (NotifyNotification *notification,
+                  GDBusMethodInvocation *invocation)
+{
+       g_debug ("Notification closed! rejecting transfer");
+       reject_transfer (invocation);
+}
+
+static void
+ask_user (GDBusMethodInvocation *invocation,
+         const gchar *filename)
+{
+       NotifyNotification *notification;
+       gchar *notification_text;
+
+       /* Translators: %s is the name of the filename being received */
+       notification_text = g_strdup_printf(_("You have been sent a file \"%s\" via Bluetooth"), filename);
+       notification = notify_notification_new (_("You have been sent a file"),
+                                               notification_text,
+                                               "bluetooth");
+
+       notify_notification_set_urgency (notification, NOTIFY_URGENCY_CRITICAL);
+       notify_notification_set_timeout (notification, NOTIFY_EXPIRES_NEVER);
+       notify_notification_set_hint_string (notification, "desktop-entry",
+                                            "gnome-bluetooth-panel");
+
+       notify_notification_add_action (notification, "receive", _("Receive"),
+                                       (NotifyActionCallback) ask_user_transfer_accepted,
+                                       invocation, NULL);
+       notify_notification_add_action (notification, "cancel", _("Cancel"),
+                                       (NotifyActionCallback) ask_user_transfer_rejected,
+                                       invocation, NULL);
+
+       /* We want to reject the transfer if the user closes the notification
+        * without accepting or rejecting it, so we connect to it. However
+        * if the user clicks on one of the actions, the callback for the
+        * action will be invoked, and then the notification will be closed
+        * and the callback for :closed will be called. So we disconnect
+        * from :closed if the invocation object goes away (which will happen
+        * after the handler of either action accepts or rejects the transfer).
+        */
+       g_signal_connect_object (G_OBJECT (notification), "closed",
+               G_CALLBACK (ask_user_on_close), invocation, 0);
+
+       if (!notify_notification_show (notification, NULL))
+               g_warning ("failed to send notification\n");
+
+       g_free (notification_text);
+}
+
+static gboolean
+get_paired_for_address (const char *adapter, const char *device)
+{
+       GtkTreeModel *model;
+       GtkTreeIter parent;
+       gboolean next;
+       gboolean ret = FALSE;
+       char *addr;
+
+       model = bluetooth_client_get_model (client);
+
+       for (next = gtk_tree_model_get_iter_first (model, &parent);
+            next;
+            next = gtk_tree_model_iter_next (model, &parent)) {
+               gtk_tree_model_get (model, &parent,
+                       BLUETOOTH_COLUMN_ADDRESS, &addr,
+                       -1);
+
+               if (g_strcmp0 (addr, adapter) == 0) {
+                       GtkTreeIter child;
+                       char *dev_addr;
+
+                       for (next = gtk_tree_model_iter_children (model, &child, &parent);
+                            next;
+                            next = gtk_tree_model_iter_next (model, &child)) {
+                               gboolean paired;
+
+                               gtk_tree_model_get (model, &child,
+                                       BLUETOOTH_COLUMN_ADDRESS, &dev_addr,
+                                       BLUETOOTH_COLUMN_PAIRED, &paired,
+                                       -1);
+                               if (g_strcmp0 (dev_addr, device) == 0) {
+                                       ret = paired;
+                                       next = FALSE;
+                               }
+                               g_free (dev_addr);
+                       }
+               }
+               g_free (addr);
+       }
+
+       g_object_unref (model);
+       return ret;
+}
+
+static void
+on_check_bonded_or_ask_session_acquired (GObject *object,
+                                        GAsyncResult *res,
+                                        gpointer user_data)
+{
+       GDBusMethodInvocation *invocation = user_data;
+       GDBusProxy *session;
+       GError *error = NULL;
+       GVariant *v;
+       char *device, *adapter;
+       gboolean paired;
+
+       session = g_dbus_proxy_new_for_bus_finish (res, &error);
+
+       if (!session) {
+               g_debug ("Failed to create a proxy for the session: %s", error->message);
+               g_clear_error (&error);
+               goto out;
+       }
+
+       device = NULL;
+       adapter = NULL;
+
+       /* obexd puts the remote device in Destination and our adapter
+        * in Source */
+       v = g_dbus_proxy_get_cached_property (session, "Destination");
+       if (v) {
+               device = g_variant_dup_string (v, NULL);
+               g_variant_unref (v);
+       }
+
+       v = g_dbus_proxy_get_cached_property (session, "Source");
+       if (v) {
+               adapter = g_variant_dup_string (v, NULL);
+               g_variant_unref (v);
+       }
+
+       g_object_unref (session);
+
+       if (!device || !adapter) {
+               g_debug ("Could not get remote device for the transfer");
+               g_free (device);
+               g_free (adapter);
+               goto out;
+       }
+
+       paired = get_paired_for_address (adapter, device);
+       g_free (device);
+       g_free (adapter);
+
+       if (paired) {
+               g_debug ("Remote device is paired, auto-accepting the transfer");
+               g_dbus_method_invocation_return_value (invocation,
+                       g_variant_new ("(s)", g_object_get_data (G_OBJECT (invocation), "temp-filename")));
+               return;
+       } else {
+               ask_user (invocation, g_object_get_data (G_OBJECT (invocation), "filename"));
+               return;
+       }
+
+out:
+       g_debug ("Rejecting transfer");
+       reject_transfer (invocation);
+}
+
+static void
+check_if_bonded_or_ask (GDBusProxy *transfer,
+                       GDBusMethodInvocation *invocation)
+{
+       GVariant *v;
+       const gchar *session = NULL;
+
+       v = g_dbus_proxy_get_cached_property (transfer, "Session");
+
+       if (v) {
+               session = g_variant_get_string (v, NULL);
+               g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION,
+                                         G_DBUS_PROXY_FLAGS_NONE,
+                                         NULL,
+                                         MANAGER_SERVICE,
+                                         session,
+                                         SESSION_IFACE,
+                                         NULL,
+                                         on_check_bonded_or_ask_session_acquired,
+                                         invocation);
+               g_variant_unref (v);
+       } else {
+               g_debug ("Could not get session path for the transfer, "
+                        "rejecting the transfer");
+               reject_transfer (invocation);
+       }
+}
+
+
+static void
+obex_agent_release (GError **error)
+{
+}
+
+static void
+obex_agent_cancel (GError **error)
+{
+}
+
+/* From the old embed/mozilla/MozDownload.cpp */
+static const char*
+file_is_compressed (const char *filename)
+{
+  int i;
+  static const char * const compression[] = {".gz", ".bz2", ".Z", ".lz", ".xz", NULL};
+
+  for (i = 0; compression[i] != NULL; i++) {
+    if (g_str_has_suffix (filename, compression[i]))
+      return compression[i];
+  }
+
+  return NULL;
+}
+
+static const char*
+parse_extension (const char *filename)
+{
+  const char *compression;
+  const char *last_separator;
+
+  compression = file_is_compressed (filename);
+
+  /* if the file is compressed we might have a double extension */
+  if (compression != NULL) {
+    int i;
+    static const char * const extensions[] = {"tar", "ps", "xcf", "dvi", "txt", "text", NULL};
+
+    for (i = 0; extensions[i] != NULL; i++) {
+      char *suffix;
+      suffix = g_strdup_printf (".%s%s", extensions[i], compression);
+
+      if (g_str_has_suffix (filename, suffix)) {
+        char *p;
+
+        p = g_strrstr (filename, suffix);
+        g_free (suffix);
+
+        return p;
+      }
+
+      g_free (suffix);
+    }
+  }
+
+  /* no compression, just look for the last dot in the filename */
+  last_separator = strrchr (filename, G_DIR_SEPARATOR);
+  return strrchr ((last_separator) ? last_separator : filename, '.');
+}
+
+char *
+lookup_download_dir (void)
+{
+       const char *special_dir;
+       char *dir;
+
+       special_dir = g_get_user_special_dir (G_USER_DIRECTORY_DOWNLOAD);
+       if (special_dir != NULL && strcmp (special_dir, g_get_home_dir ()) != 0) {
+               g_mkdir_with_parents (special_dir, 0755);
+               return g_strdup (special_dir);
+       }
+
+       dir = g_build_filename (g_get_home_dir (), "Downloads", NULL);
+       g_mkdir_with_parents (dir, 0755);
+       return dir;
+}
+
+static char *
+move_temp_filename (GObject *object)
+{
+       const char *orig_filename;
+       char *dest_filename, *dest_dir;
+       GFile *src, *dest;
+       GError *error = NULL;
+       gboolean res;
+
+       orig_filename = g_object_get_data (object, "temp-filename");
+       src = g_file_new_for_path (orig_filename);
+
+       dest_dir = lookup_download_dir ();
+       dest_filename = g_build_filename (dest_dir, g_object_get_data (object, "filename"), NULL);
+       g_free (dest_dir);
+       dest = g_file_new_for_path (dest_filename);
+
+       res = g_file_move (src, dest,
+                          G_FILE_COPY_NONE, NULL,
+                          NULL, NULL, &error);
+
+       /* This is sync, but the files will be on the same partition already
+        * (~/.cache/obexd to ~/Downloads) */
+       if (!res && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
+               guint i = 1;
+               const char *dot_pos;
+               gssize position;
+               char *serial = NULL;
+               GString *tmp_filename;
+
+               dot_pos = parse_extension (dest_filename);
+               if (dot_pos)
+                       position = dot_pos - dest_filename;
+               else
+                       position = strlen (dest_filename);
+
+               tmp_filename = g_string_new (NULL);
+               g_string_assign (tmp_filename, dest_filename);
+
+               while (!res && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
+                       g_debug ("Couldn't move file to %s", tmp_filename->str);
+
+                       g_clear_error (&error);
+                       g_object_unref (dest);
+
+                       serial = g_strdup_printf ("(%d)", i++);
+
+                       g_string_assign (tmp_filename, dest_filename);
+                       g_string_insert (tmp_filename, position, serial);
+
+                       g_free (serial);
+
+                       dest = g_file_new_for_path (tmp_filename->str);
+                       res = g_file_move (src, dest,
+                                          G_FILE_COPY_NONE, NULL,
+                                          NULL, NULL, &error);
+               }
+
+               g_free (dest_filename);
+               dest_filename = g_strdup (tmp_filename->str);
+               g_string_free (tmp_filename, TRUE);
+       }
+
+       if (!res) {
+               g_warning ("Failed to move %s to %s: '%s'",
+                          orig_filename, dest_filename, error->message);
+               g_error_free (error);
+       } else {
+               g_debug ("Moved %s (orig name %s) to %s",
+                        orig_filename, (char *) g_object_get_data (object, "filename"), dest_filename);
+       }
+
+       g_object_unref (src);
+       g_object_unref (dest);
+
+       return dest_filename;
+}
+
+static void
+transfer_property_changed (GDBusProxy *transfer,
+                          GVariant   *changed_properties,
+                          GStrv       invalidated_properties,
+                          gpointer    user_data)
+{
+       GVariantIter iter;
+       const gchar *key;
+       GVariant *value;
+       const char *filename;
+
+       g_debug ("Calling transfer_property_changed()");
+
+       filename = g_object_get_data (G_OBJECT (transfer), "filename");
+
+       g_variant_iter_init (&iter, changed_properties);
+       while (g_variant_iter_next (&iter, "{&sv}", &key, &value)) {
+               char *str = g_variant_print (value, TRUE);
+
+               if (g_str_equal (key, "Status")) {
+                       const gchar *status;
+
+                       status = g_variant_get_string (value, NULL);
+
+                       g_debug ("Got status %s = %s for filename %s", status, str, filename);
+
+                       if (g_str_equal (status, "complete")) {
+                               char *path;
+
+                               path = move_temp_filename (G_OBJECT (transfer));
+                               g_debug ("transfer completed, showing a notification");
+                               show_notification (path);
+                               g_free (path);
+                       }
+
+                       /* Done with this transfer */
+                       if (g_str_equal (status, "complete") ||
+                           g_str_equal (status, "error")) {
+                               g_object_unref (transfer);
+                       }
+               } else {
+                       g_debug ("Unhandled property changed %s = %s for filename %s", key, str, filename);
+               }
+               g_free (str);
+               g_variant_unref (value);
+       }
+}
+
+static void
+obex_agent_authorize_push (GObject *source_object,
+                          GAsyncResult *res,
+                          gpointer user_data)
+{
+       GDBusProxy *transfer = g_dbus_proxy_new_for_bus_finish (res, NULL);
+       GDBusMethodInvocation *invocation = user_data;
+       GVariant *variant = g_dbus_proxy_get_cached_property (transfer, "Name");
+       const gchar *filename = g_variant_get_string (variant, NULL);
+       char *template;
+       int fd;
+
+       g_debug ("AuthorizePush received");
+
+       template = g_build_filename (g_get_user_cache_dir (), "obexd", "XXXXXX", NULL);
+       fd = g_mkstemp (template);
+       close (fd);
+
+       g_object_set_data_full (G_OBJECT (transfer), "filename", g_strdup (filename), g_free);
+       g_object_set_data_full (G_OBJECT (transfer), "temp-filename", g_strdup (template), g_free);
+
+       g_object_set_data_full (G_OBJECT (invocation), "filename", g_strdup (filename), g_free);
+       g_object_set_data_full (G_OBJECT (invocation), "temp-filename", g_strdup (template), g_free);
+
+       g_signal_connect (transfer, "g-properties-changed",
+                         G_CALLBACK (transfer_property_changed), NULL);
+
+       /* check_if_bonded_or_ask() will accept or reject the transfer */
+       check_if_bonded_or_ask (transfer, invocation);
+       g_variant_unref (variant);
+       g_free (template);
+}
+
+static void
+handle_method_call (GDBusConnection       *connection,
+                   const gchar           *sender,
+                   const gchar           *object_path,
+                   const gchar           *interface_name,
+                   const gchar           *method_name,
+                   GVariant              *parameters,
+                   GDBusMethodInvocation *invocation,
+                   gpointer               user_data)
+{
+       if (g_str_equal (method_name, "Cancel")) {
+               obex_agent_cancel (NULL);
+               g_dbus_method_invocation_return_value (invocation, NULL);
+       } else if (g_str_equal (method_name, "Release")) {
+               obex_agent_release (NULL);
+               g_dbus_method_invocation_return_value (invocation, NULL);
+       } else if (g_str_equal (method_name, "AuthorizePush")) {
+               const gchar *transfer;
+
+               g_variant_get (parameters, "(&o)", &transfer);
+
+               g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION,
+                                         G_DBUS_PROXY_FLAGS_NONE,
+                                         NULL,
+                                         MANAGER_SERVICE,
+                                         transfer,
+                                         TRANSFER_IFACE,
+                                         NULL,
+                                         obex_agent_authorize_push,
+                                         invocation);
+       } else {
+               g_warning ("Unknown method name or unknown parameters: %s",
+                          method_name);
+       }
+}
+
+static const GDBusInterfaceVTable interface_vtable =
+{
+  handle_method_call,
+  NULL,
+  NULL
+};
+
+static void
+obexd_appeared_cb (GDBusConnection *connection,
+                  const gchar *name,
+                  const gchar *name_owner,
+                  gpointer user_data)
+{
+       ObexAgent *self = user_data;
+
+       g_debug ("obexd appeared, registering agent");
+       g_dbus_connection_call (self->connection,
+                               MANAGER_SERVICE,
+                               MANAGER_PATH,
+                               MANAGER_IFACE,
+                               "RegisterAgent",
+                               g_variant_new ("(o)", AGENT_PATH),
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NONE,
+                               -1,
+                               NULL,
+                               NULL,
+                               NULL);
+}
+
+static void
+on_bus_acquired (GDBusConnection *connection,
+                const gchar     *name,
+                gpointer         user_data)
+{
+       ObexAgent *self = user_data;
+
+       /* parse introspection data */
+       introspection_data = g_dbus_node_info_new_for_xml (introspection_xml,
+                                                          NULL);
+
+       self->connection = connection;
+       self->object_reg_id = g_dbus_connection_register_object (connection,
+                                                                AGENT_PATH,
+                                                                introspection_data->interfaces[0],
+                                                                &interface_vtable,
+                                                                NULL,  /* user_data */
+                                                                NULL,  /* user_data_free_func */
+                                                                NULL); /* GError** */
+
+       g_dbus_node_info_unref (introspection_data);
+
+       g_assert (self->object_reg_id > 0);
+
+       self->obexd_watch_id = g_bus_watch_name_on_connection (self->connection,
+                                                              MANAGER_SERVICE,
+                                                              G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+                                                              obexd_appeared_cb,
+                                                              NULL,
+                                                              self,
+                                                              NULL);
+}
+
+static void
+obex_agent_init (ObexAgent *self)
+{
+       self->owner_id = g_bus_own_name (G_BUS_TYPE_SESSION,
+                                        AGENT_IFACE,
+                                        G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT,
+                                        on_bus_acquired,
+                                        NULL,
+                                        NULL,
+                                        self,
+                                        NULL);
+
+       client = bluetooth_client_new ();
+}
+
+static void
+obex_agent_dispose (GObject *obj)
+{
+       ObexAgent *self = OBEX_AGENT (obj);
+
+       g_dbus_connection_unregister_object (self->connection, self->object_reg_id);
+       self->object_reg_id = 0;
+
+       g_bus_unown_name (self->owner_id);
+       self->owner_id = 0;
+
+       g_bus_unwatch_name (self->obexd_watch_id);
+       self->obexd_watch_id = 0;
+
+       g_clear_object (&client);
+
+       G_OBJECT_CLASS (obex_agent_parent_class)->dispose (obj);
+}
+
+static void
+obex_agent_class_init (ObexAgentClass *klass)
+{
+       GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+       gobject_class->dispose = obex_agent_dispose;
+}
+
+static ObexAgent *
+obex_agent_new (void)
+{
+       return (ObexAgent *) g_object_new (OBEX_AGENT_TYPE, NULL);
+}
+
+void
+obex_agent_down (void)
+{
+       if (agent == NULL)
+               return;
+
+       g_dbus_connection_call (agent->connection,
+                               MANAGER_SERVICE,
+                               MANAGER_PATH,
+                               MANAGER_IFACE,
+                               "UnregisterAgent",
+                               g_variant_new ("(o)", AGENT_PATH),
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NONE,
+                               -1,
+                               NULL,
+                               NULL,
+                               NULL);
+
+       g_clear_object (&agent);
+}
+
+void
+obex_agent_up (void)
+{
+       if (agent == NULL)
+               agent = obex_agent_new ();
+
+       if (!notify_init("gnome-user-share")) {
+               g_warning("Unable to initialize the notification system");
+       }
+}
diff --git a/lib/bluetooth-settings-obexpush.h b/lib/bluetooth-settings-obexpush.h
new file mode 100644
index 0000000..272968e
--- /dev/null
+++ b/lib/bluetooth-settings-obexpush.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+
+/*
+ *  Copyright (C) 2004-2008 Red Hat, Inc.
+ *  Copyright (C) 2013 Intel Corporation.
+ *
+ *  Nautilus is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  Nautilus 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
+ *  General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ *  Authors: Bastien Nocera <hadess hadess net>
+ *  Authors: Emilio Pozuelo Monfort <emilio pozuelo collabora co uk>
+ *
+ */
+
+#ifndef __OBEX_AGENT_H__
+#define __OBEX_AGENT_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _ObexAgent {
+       GObject parent;
+       guint owner_id;
+       guint object_reg_id;
+       guint obexd_watch_id;
+       GDBusConnection *connection;
+} ObexAgent;
+
+typedef struct _ObexAgentClass {
+       GObjectClass parent;
+} ObexAgentClass;
+
+GType obex_agent_get_type (void);
+
+#define OBEX_AGENT_TYPE              (obex_agent_get_type ())
+#define OBEX_AGENT(object)           (G_TYPE_CHECK_INSTANCE_CAST ((object), OBEX_AGENT_TYPE, ObexAgent))
+#define OBEX_AGENT_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST ((klass), OBEX_AGENT_TYPE, ObexAgentClass))
+#define IS_OBEX_AGENT(object)        (G_TYPE_CHECK_INSTANCE_TYPE ((object), OBEX_AGENT_TYPE))
+#define IS_OBEX_AGENT_CLASS(klass)   (G_TYPE_CHECK_CLASS_TYPE ((klass), OBEX_AGENT_TYPE))
+#define OBEX_AGENT_GET_CLASS(obj)    (G_TYPE_INSTANCE_GET_CLASS ((obj), OBEX_AGENT_TYPE, ObexAgentClass))
+
+void     obex_agent_up (void);
+void     obex_agent_down (void);
+char    *lookup_download_dir (void);
+
+G_END_DECLS
+
+#endif
diff --git a/lib/bluetooth-settings-widget.c b/lib/bluetooth-settings-widget.c
index 3efe0fe..07293b1 100644
--- a/lib/bluetooth-settings-widget.c
+++ b/lib/bluetooth-settings-widget.c
@@ -35,6 +35,7 @@
 #include "bluetooth-settings-widget.h"
 #include "bluetooth-settings-resources.h"
 #include "bluetooth-settings-row.h"
+#include "bluetooth-settings-obexpush.h"
 #include "bluetooth-pairing-dialog.h"
 #include "pin.h"
 
@@ -75,6 +76,10 @@ struct _BluetoothSettingsWidgetPrivate {
 
        /* Visible */
        GtkWidget           *visible_label;
+
+       /* Sharing section */
+       gboolean             has_console;
+       GDBusProxy          *session_proxy;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE(BluetoothSettingsWidget, bluetooth_settings_widget, GTK_TYPE_BOX)
@@ -103,6 +108,10 @@ static guint signals[LAST_SIGNAL] = { 0 };
 
 #define AGENT_PATH "/org/gnome/bluetooth/settings"
 
+#define GNOME_SESSION_DBUS_NAME      "org.gnome.SessionManager"
+#define GNOME_SESSION_DBUS_OBJECT    "/org/gnome/SessionManager"
+#define GNOME_SESSION_DBUS_INTERFACE "org.gnome.SessionManager"
+
 enum {
        CONNECTING_NOTEBOOK_PAGE_SWITCH = 0,
        CONNECTING_NOTEBOOK_PAGE_SPINNER = 1
@@ -1159,13 +1168,19 @@ update_visibility (BluetoothSettingsWidget *self)
 
        g_object_get (G_OBJECT (priv->client), "default-adapter-name", &name, NULL);
        if (name != NULL) {
-               char *label;
+               char *label, *path, *uri;
 
-               /* translators: %s is the name of the computer, for example:
-                * Visible as “Bastien Nocera's Computer” */
-               label = g_strdup_printf (_("Visible as “%s”"), name);
+               path = lookup_download_dir ();
+               uri = g_filename_to_uri (path, NULL, NULL);
+               g_free (path);
+
+               /* translators: first %s is the name of the computer, for example:
+                * Visible as “Bastien Nocera's Computer” followed by the
+                * location of the Downloads folder.*/
+               label = g_strdup_printf (_("Visible as “%s” and available for Bluetooth file transfers. 
Transferred files are placed in the <a href=\"%s\">Downloads</a> folder."), name, uri);
+               g_free (uri);
                g_free (name);
-               gtk_label_set_text (GTK_LABEL (priv->visible_label), label);
+               gtk_label_set_markup (GTK_LABEL (priv->visible_label), label);
                g_free (label);
        }
        gtk_widget_set_visible (priv->visible_label, name != NULL);
@@ -1462,10 +1477,8 @@ add_device_section (BluetoothSettingsWidget *self)
 
        /* Discoverable label placeholder, the real name is set in update_visibility().
         * If you ever see this string during normal use, please file a bug. */
-       priv->visible_label = gtk_label_new ("Visible as “Foobar”");
+       priv->visible_label = WID ("explanation-label");
        gtk_label_set_use_markup (GTK_LABEL (priv->visible_label), TRUE);
-       gtk_misc_set_alignment (GTK_MISC (priv->visible_label), 1.0, 0.5);
-       gtk_box_pack_end (GTK_BOX (hbox), priv->visible_label, FALSE, TRUE, 0);
        update_visibility (self);
 
        priv->device_list = widget = gtk_list_box_new ();
@@ -1490,22 +1503,6 @@ add_device_section (BluetoothSettingsWidget *self)
        gtk_widget_show_all (box);
 }
 
-static void
-on_content_size_changed (GtkWidget *widget, GtkAllocation *allocation, gpointer data)
-{
-       GtkWidget *box;
-
-       box = gtk_widget_get_parent (gtk_widget_get_parent (widget));
-       if (allocation->height < 490) {
-               gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box),
-                                               GTK_POLICY_NEVER, GTK_POLICY_NEVER);
-       } else {
-               gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box),
-                                               GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
-               gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (box), 490);
-       }
-}
-
 static gboolean
 is_interesting_device (GtkTreeModel *model,
                       GtkTreeIter  *iter)
@@ -1741,10 +1738,77 @@ setup_pairing_agent (BluetoothSettingsWidget *self)
 }
 
 static void
+session_properties_changed_cb (GDBusProxy               *session,
+                              GVariant                 *changed,
+                              char                    **invalidated,
+                              BluetoothSettingsWidget  *self)
+{
+       BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self);
+       GVariant *v;
+
+       v = g_variant_lookup_value (changed, "SessionIsActive", G_VARIANT_TYPE_BOOLEAN);
+       if (v) {
+               priv->has_console = g_variant_get_boolean (v);
+               g_debug ("Received session is active change: now %s", priv->has_console ? "active" : 
"inactive");
+               g_variant_unref (v);
+
+               if (priv->has_console)
+                       obex_agent_up ();
+               else
+                       obex_agent_down ();
+       }
+}
+
+static gboolean
+is_session_active (BluetoothSettingsWidget *self)
+{
+       BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self);
+       GVariant *variant;
+       gboolean is_session_active = FALSE;
+
+       variant = g_dbus_proxy_get_cached_property (priv->session_proxy,
+                                                   "SessionIsActive");
+       if (variant) {
+               is_session_active = g_variant_get_boolean (variant);
+               g_variant_unref (variant);
+       }
+
+       return is_session_active;
+}
+
+static void
+setup_obex (BluetoothSettingsWidget *self)
+{
+       BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self);
+       GError *error = NULL;
+
+       priv->session_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+                                                            G_DBUS_PROXY_FLAGS_NONE,
+                                                            NULL,
+                                                            GNOME_SESSION_DBUS_NAME,
+                                                            GNOME_SESSION_DBUS_OBJECT,
+                                                            GNOME_SESSION_DBUS_INTERFACE,
+                                                            NULL,
+                                                            &error);
+
+       if (priv->session_proxy == NULL) {
+               g_warning ("Failed to get session proxy: %s", error->message);
+               g_error_free (error);
+               return;
+       }
+       g_signal_connect (priv->session_proxy, "g-properties-changed",
+                         G_CALLBACK (session_properties_changed_cb), self);
+       priv->has_console = is_session_active (self);
+
+       if (priv->has_console)
+               obex_agent_up ();
+}
+
+static void
 bluetooth_settings_widget_init (BluetoothSettingsWidget *self)
 {
        BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self);
-       GtkWidget *widget, *box;
+       GtkWidget *widget;
        GError *error = NULL;
 
        priv->cancellable = g_cancellable_new ();
@@ -1762,7 +1826,7 @@ bluetooth_settings_widget_init (BluetoothSettingsWidget *self)
                return;
        }
 
-       widget = WID ("vbox_bluetooth");
+       widget = WID ("scrolledwindow1");
 
        priv->connecting_devices = g_hash_table_new_full (g_str_hash,
                                                                g_str_equal,
@@ -1796,22 +1860,16 @@ bluetooth_settings_widget_init (BluetoothSettingsWidget *self)
 
        add_device_section (self);
 
-       box = gtk_scrolled_window_new (NULL, NULL);
-       gtk_widget_set_hexpand (box, TRUE);
-       gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box),
+       gtk_widget_set_hexpand (widget, TRUE);
+       gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (widget),
                                        GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
-       g_signal_connect (widget, "size-allocate",
-                         G_CALLBACK (on_content_size_changed), NULL);
-       gtk_widget_show (box);
-       gtk_container_add (GTK_CONTAINER (self), box);
-       gtk_container_add (GTK_CONTAINER (box), widget);
-
-       priv->focus_adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (box));
-       gtk_container_set_focus_vadjustment (GTK_CONTAINER (widget), priv->focus_adjustment);
+       gtk_container_add (GTK_CONTAINER (self), widget);
 
        setup_properties_dialog (self);
 
        gtk_widget_show_all (GTK_WIDGET (self));
+
+       setup_obex (self);
 }
 
 static void
@@ -1823,6 +1881,7 @@ bluetooth_settings_widget_finalize (GObject *object)
        g_clear_object (&priv->agent);
        g_clear_pointer (&priv->properties_dialog, gtk_widget_destroy);
        g_clear_pointer (&priv->pairing_dialog, gtk_widget_destroy);
+       g_clear_object (&priv->session_proxy);
 
        /* See default_adapter_changed () */
        if (priv->client)
diff --git a/lib/settings.ui b/lib/settings.ui
index 6e23016..3c2b5dc 100644
--- a/lib/settings.ui
+++ b/lib/settings.ui
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.18.3 -->
+<!-- Generated with glade 3.19.0 -->
 <interface>
-  <requires lib="gtk+" version="3.0"/>
+  <requires lib="gtk+" version="3.16"/>
   <object class="GtkBox" id="properties_vbox">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
@@ -23,7 +23,7 @@
             <property name="margin_top">16</property>
             <property name="margin_bottom">16</property>
             <property name="icon_name">image-missing</property>
-            <property name="use-fallback">True</property>
+            <property name="use_fallback">True</property>
           </object>
           <packing>
             <property name="expand">False</property>
@@ -41,8 +41,8 @@
               <object class="GtkLabel" id="connection_label">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">1</property>
                 <property name="label" translatable="yes">Connection</property>
+                <property name="xalign">1</property>
                 <style>
                   <class name="dim-label"/>
                 </style>
@@ -71,7 +71,7 @@
                   <object class="GtkLabel" id="label3">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
-                    <property name="label" translatable="yes">page 1</property>
+                    <property name="label">page 1</property>
                   </object>
                   <packing>
                     <property name="tab_fill">False</property>
@@ -91,7 +91,7 @@
                   <object class="GtkLabel" id="label9">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
-                    <property name="label" translatable="yes">page 2</property>
+                    <property name="label">page 2</property>
                   </object>
                   <packing>
                     <property name="position">1</property>
@@ -131,8 +131,8 @@
               <object class="GtkLabel" id="label4">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">1</property>
                 <property name="label" translatable="yes">Paired</property>
+                <property name="xalign">1</property>
                 <style>
                   <class name="dim-label"/>
                 </style>
@@ -147,8 +147,8 @@
               <object class="GtkLabel" id="paired_label">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">0</property>
                 <property name="label">Yes</property>
+                <property name="xalign">0</property>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -176,8 +176,8 @@
               <object class="GtkLabel" id="label5">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">1</property>
                 <property name="label" translatable="yes">Type</property>
+                <property name="xalign">1</property>
                 <style>
                   <class name="dim-label"/>
                 </style>
@@ -192,8 +192,8 @@
               <object class="GtkLabel" id="type_label">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">0</property>
                 <property name="label">Keyboard</property>
+                <property name="xalign">0</property>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -221,8 +221,8 @@
               <object class="GtkLabel" id="label6">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">1</property>
                 <property name="label" translatable="yes">Address</property>
+                <property name="xalign">1</property>
                 <style>
                   <class name="dim-label"/>
                 </style>
@@ -237,9 +237,9 @@
               <object class="GtkLabel" id="address_label">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="xalign">0</property>
                 <property name="label">00:00:00:00:00</property>
                 <property name="selectable">True</property>
+                <property name="xalign">0</property>
               </object>
               <packing>
                 <property name="expand">False</property>
@@ -364,12 +364,43 @@
       </packing>
     </child>
   </object>
-  <object class="GtkBox" id="vbox_bluetooth">
+  <object class="GtkScrolledWindow" id="scrolledwindow1">
     <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="margin_top">12</property>
-    <property name="border_width">12</property>
-    <property name="orientation">vertical</property>
-    <property name="spacing">3</property>
+    <property name="can_focus">True</property>
+    <property name="shadow_type">in</property>
+    <child>
+      <object class="GtkViewport" id="viewport1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkBox" id="vbox_bluetooth">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkLabel" id="explanation-label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="margin_top">16</property>
+                <property name="margin_bottom">8</property>
+                <property name="label">Visible as "Foobar" and available for Bluetooth file transfers. 
Transferred files are placed in the Downloads folder.</property>
+                <property name="wrap">True</property>
+                <property name="width_chars">50</property>
+                <property name="max_width_chars">50</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
   </object>
 </interface>
diff --git a/lib/test-settings.c b/lib/test-settings.c
index 0e42488..3890e5f 100644
--- a/lib/test-settings.c
+++ b/lib/test-settings.c
@@ -16,7 +16,7 @@ int main (int argc, char **argv)
        gtk_init (&argc, &argv);
 
        window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
-       gtk_widget_set_size_request (window, 740, -1);
+       gtk_widget_set_size_request (window, 740, 490);
        gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
        g_signal_connect (G_OBJECT (window), "delete-event",
                          G_CALLBACK (delete_event_cb), NULL);
diff --git a/po/POTFILES.in b/po/POTFILES.in
index dcb0bb7..ca0b6e3 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,6 +7,7 @@ lib/bluetooth-pairing-dialog.c
 lib/bluetooth-settings-row.c
 [type: gettext/glade]lib/bluetooth-settings-row.ui
 lib/bluetooth-settings-widget.c
+lib/bluetooth-settings-obexpush.c
 lib/bluetooth-utils.c
 [type: gettext/glade]lib/settings.ui
 sendto/bluetooth-sendto.desktop.in.in



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]