/*
* Copyright 2014 Canonical Ltd.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3, as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranties of
* MERCHANTABILITY, SATISFACTORY QUALITY, 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, see .
*
* Authors:
* Charles Kerr
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace unity {
namespace indicator {
namespace datetime {
/***
****
***/
namespace
{
/**
* Plays a sound in a loop.
*/
class LoopedSound
{
typedef LoopedSound Self;
public:
LoopedSound(const std::string& filename, const AlarmVolume volume):
m_filename(filename),
m_volume(volume),
m_canberra_id(get_next_canberra_id())
{
const auto rv = ca_context_create(&m_context);
if (rv == CA_SUCCESS)
{
play();
}
else
{
g_warning("Failed to create canberra context: %s", ca_strerror(rv));
m_context = nullptr;
}
}
~LoopedSound()
{
stop();
g_clear_pointer(&m_context, ca_context_destroy);
}
private:
void stop()
{
if (m_context != nullptr)
{
const auto rv = ca_context_cancel(m_context, m_canberra_id);
if (rv != CA_SUCCESS)
g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
}
if (m_timeout_tag != 0)
{
g_source_remove(m_timeout_tag);
m_timeout_tag = 0;
}
}
static void on_done_playing(ca_context*, uint32_t /*id*/, int rv, void* gself)
{
auto self = static_cast(gself);
// wait a second, then play it again
if ((rv == CA_SUCCESS) && (self->m_timeout_tag == 0))
self->m_timeout_tag = g_timeout_add_seconds(1, play_idle, self);
}
static gboolean play_idle(gpointer gself)
{
auto self = static_cast(gself);
self->m_timeout_tag = 0;
self->play();
return G_SOURCE_REMOVE;
}
void play()
{
auto context = m_context;
g_return_if_fail(context != nullptr);
ca_proplist* props = nullptr;
ca_proplist_create(&props);
ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, m_filename.c_str());
ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", get_decibel_multiplier(m_volume));
const auto rv = ca_context_play_full(context, m_canberra_id, props, on_done_playing, this);
if (rv != CA_SUCCESS)
g_warning("Failed to play file '%s': %s", m_filename.c_str(), ca_strerror(rv));
g_clear_pointer(&props, ca_proplist_destroy);
}
static float get_decibel_multiplier(const AlarmVolume volume)
{
/* These values aren't set in stone --
arrived at from from manual tests on Nexus 4 */
switch (volume)
{
case ALARM_VOLUME_VERY_QUIET: return -8;
case ALARM_VOLUME_QUIET: return -4;
case ALARM_VOLUME_VERY_LOUD: return 8;
case ALARM_VOLUME_LOUD: return 4;
default: return 0;
}
}
/***
****
***/
static int32_t get_next_canberra_id()
{
static int32_t next_canberra_id = 1;
return next_canberra_id++;
}
ca_context* m_context = nullptr;
guint m_timeout_tag = 0;
const std::string m_filename;
const AlarmVolume m_volume;
const int32_t m_canberra_id;
};
/**
* A popup notification (with optional sound)
* that emits a Response signal when done.
*/
class Popup
{
public:
Popup(const Appointment& appointment,
const std::string& sound_filename,
const AlarmVolume sound_volume):
m_appointment(appointment),
m_sound_filename(sound_filename),
m_sound_volume(sound_volume),
m_mode(get_mode())
{
show();
}
~Popup()
{
if (m_nn != nullptr)
{
notify_notification_clear_actions(m_nn);
g_signal_handlers_disconnect_by_data(m_nn, this);
g_clear_object(&m_nn);
}
}
typedef enum
{
RESPONSE_SHOW,
RESPONSE_DISMISS,
RESPONSE_CLOSE
}
Response;
core::Signal& response() { return m_response; }
private:
void show()
{
const Appointment& appointment = m_appointment;
const auto timestr = appointment.begin.format("%a, %X");
auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
const auto body = appointment.summary;
const gchar* icon_name = "alarm-clock";
m_nn = notify_notification_new(title, body.c_str(), icon_name);
if (m_mode == MODE_SNAP)
{
notify_notification_set_hint_string(m_nn, "x-canonical-snap-decisions", "true");
notify_notification_set_hint_string(m_nn, "x-canonical-private-button-tint", "true");
// text for the alarm popup dialog's button to show the active alarm
notify_notification_add_action(m_nn, "show", _("Show"), on_snap_show, this, nullptr);
// text for the alarm popup dialog's button to shut up the alarm
notify_notification_add_action(m_nn, "dismiss", _("Dismiss"), on_snap_dismiss, this, nullptr);
g_signal_connect(m_nn, "closed", G_CALLBACK(on_snap_closed), this);
}
bool shown = true;
GError* error = nullptr;
notify_notification_show(m_nn, &error);
if (error != NULL)
{
g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
g_error_free(error);
shown = false;
}
// if we were able to show a popup that requires user response,
// play the sound in a loop
if (shown && (m_mode == MODE_SNAP))
m_sound.reset(new LoopedSound(m_sound_filename, m_sound_volume));
// if showing the notification didn't work,
// treat it as if the user clicked the 'show' button
if (!shown)
{
on_snap_show(nullptr, nullptr, this);
on_snap_dismiss(nullptr, nullptr, this);
}
g_free(title);
}
// user clicked 'show'
static void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gself)
{
auto self = static_cast(gself);
self->m_response_value = RESPONSE_SHOW;
self->m_sound.reset();
}
// user clicked 'dismiss'
static void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gself)
{
auto self = static_cast(gself);
self->m_response_value = RESPONSE_DISMISS;
self->m_sound.reset();
}
// the popup was closed
static void on_snap_closed(NotifyNotification*, gpointer gself)
{
auto self = static_cast(gself);
self->m_sound.reset();
self->m_response(self->m_response_value);
}
/***
****
***/
static std::set get_server_caps()
{
std::set caps_set;
auto caps_gl = notify_get_server_caps();
std::string caps_str;
for(auto l=caps_gl; l!=nullptr; l=l->next)
{
caps_set.insert((const char*)l->data);
caps_str += (const char*) l->data;;
if (l->next != nullptr)
caps_str += ", ";
}
g_debug("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
g_list_free_full(caps_gl, g_free);
return caps_set;
}
typedef enum
{
// just a bubble... no actions, no audio
MODE_BUBBLE,
// a snap decision popup dialog + audio
MODE_SNAP
}
Mode;
static Mode get_mode()
{
static Mode mode;
static bool mode_inited = false;
if (G_UNLIKELY(!mode_inited))
{
const auto caps = get_server_caps();
if (caps.count("actions"))
mode = MODE_SNAP;
else
mode = MODE_BUBBLE;
mode_inited = true;
}
return mode;
}
/***
****
***/
typedef Popup Self;
const Appointment m_appointment;
const std::string m_sound_filename;
const AlarmVolume m_sound_volume;
const Mode m_mode;
std::unique_ptr m_sound;
core::Signal m_response;
Response m_response_value = RESPONSE_CLOSE;
NotifyNotification* m_nn = nullptr;
};
/**
*** libnotify -- snap decisions
**/
void first_time_init()
{
static bool inited = false;
if (G_UNLIKELY(!inited))
{
inited = true;
if(!notify_init("indicator-datetime-service"))
g_critical("libnotify initialization failed");
}
}
std::string get_local_filename (const std::string& str)
{
std::string ret;
// maybe try it as a file path
if (ret.empty() && !str.empty())
{
auto file = g_file_new_for_path (str.c_str());
if (g_file_is_native(file) && g_file_query_exists(file,nullptr))
ret = g_file_get_path(file);
g_clear_object(&file);
}
// maybe try it as a uri
if (ret.empty() && !str.empty())
{
auto file = g_file_new_for_uri (str.c_str());
if (g_file_is_native(file) && g_file_query_exists(file,nullptr))
ret = g_file_get_path(file);
g_clear_object(&file);
}
return ret;
}
std::string get_alarm_sound(const Appointment& appointment,
const std::shared_ptr& settings)
{
static const constexpr char* const FALLBACK_AUDIO_FILENAME {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
const std::string candidates[] = { appointment.audio_url,
settings->alarm_sound.get(),
FALLBACK_AUDIO_FILENAME };
std::string alarm_sound;
for (const auto& candidate : candidates)
{
alarm_sound = get_local_filename (candidate);
if (!alarm_sound.empty())
break;
}
g_debug("%s: Appointment \"%s\" using alarm sound \"%s\"",
G_STRFUNC, appointment.summary.c_str(), alarm_sound.c_str());
return alarm_sound;
}
} // unnamed namespace
/***
****
***/
Snap::Snap(const std::shared_ptr& settings):
m_settings(settings)
{
first_time_init();
}
Snap::~Snap()
{
}
void Snap::operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss)
{
if (!appointment.has_alarms)
{
dismiss(appointment);
return;
}
// create a popup...
auto popup = new Popup(appointment,
get_alarm_sound(appointment, m_settings),
m_settings->alarm_volume.get());
// listen for it to finish...
popup->response().connect([appointment,
show,
dismiss,
popup](Popup::Response response){
// we can't delete the Popup inside its response() signal handler,
// so push that to an idle func
g_idle_add([](gpointer gdata){
delete static_cast(gdata);
return G_SOURCE_REMOVE;
}, popup);
// maybe notify the client code that the popup's done
if (response == Popup::RESPONSE_SHOW)
show(appointment);
else if (response == Popup::RESPONSE_DISMISS)
dismiss(appointment);
});
}
/***
****
***/
} // namespace datetime
} // namespace indicator
} // namespace unity