/*
* 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, possibly looping.
*/
class Sound
{
typedef Sound Self;
public:
Sound(const std::shared_ptr& clock,
const std::string& filename,
unsigned int volume,
unsigned int duration_minutes,
bool loop):
m_clock(clock),
m_filename(filename),
m_volume(volume),
m_loop(loop),
m_canberra_id(get_next_canberra_id()),
m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, duration_minutes, 0.0))
{
if (m_loop)
{
g_debug("Looping '%s' until cutoff time %s",
m_filename.c_str(),
m_loop_end_time.format("%F %T").c_str());
}
else
{
g_debug("Playing '%s' once", m_filename.c_str());
}
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;
}
}
~Sound()
{
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_loop_tag != 0)
{
g_source_remove(m_loop_tag);
m_loop_tag = 0;
}
}
void play()
{
auto context = m_context;
g_return_if_fail(context != nullptr);
const auto filename = m_filename.c_str();
const float gain = get_gain_level(m_volume);
ca_proplist* props = nullptr;
ca_proplist_create(&props);
ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", gain);
const auto rv = ca_context_play_full(context, m_canberra_id, props,
on_done_playing, this);
if (rv != CA_SUCCESS)
g_warning("Unable to play '%s': %s", filename, ca_strerror(rv));
g_clear_pointer(&props, ca_proplist_destroy);
}
static float get_gain_level(unsigned int volume)
{
const unsigned int clamped_volume = CLAMP(volume, 1, 100);
/* This range isn't set in stone --
arrived at from manual tests on Nextus 4 */
constexpr float gain_low = -10;
constexpr float gain_high = 10;
constexpr float gain_range = gain_high - gain_low;
return gain_low + (gain_range * (clamped_volume / 100.0f));
}
static void on_done_playing(ca_context*, uint32_t, int rv, void* gself)
{
// if we still need to loop, wait a second, then play it again
if (rv == CA_SUCCESS)
{
auto self = static_cast(gself);
if ((self->m_loop_tag == 0) &&
(self->m_loop) &&
(self->m_clock->localtime() < self->m_loop_end_time))
{
self->m_loop_tag = g_timeout_add_seconds(1, play_idle, self);
}
}
}
static gboolean play_idle(gpointer gself)
{
auto self = static_cast(gself);
self->m_loop_tag = 0;
self->play();
return G_SOURCE_REMOVE;
}
/***
****
***/
static int32_t get_next_canberra_id()
{
static int32_t next_canberra_id = 1;
return next_canberra_id++;
}
const std::shared_ptr m_clock;
const std::string m_filename;
const unsigned int m_volume;
const bool m_loop;
const int32_t m_canberra_id;
const DateTime m_loop_end_time;
ca_context* m_context = nullptr;
guint m_loop_tag = 0;
};
class SoundBuilder
{
public:
void set_clock(const std::shared_ptr& c) {m_clock = c;}
void set_filename(const std::string& s) {m_filename = s;}
void set_volume(const unsigned int v) {m_volume = v;}
void set_duration_minutes(int unsigned i) {m_duration_minutes=i;}
void set_looping(bool b) {m_looping=b;}
Sound* operator()() {
return new Sound (m_clock,
m_filename,
m_volume,
m_duration_minutes,
m_looping);
}
private:
std::shared_ptr m_clock;
std::string m_filename;
unsigned int m_volume = 50;
unsigned int m_duration_minutes = 30;
bool m_looping = true;
};
/**
* A popup notification (with optional sound)
* that emits a Response signal when done.
*/
class Popup
{
public:
Popup(const Appointment& appointment, const SoundBuilder& sound_builder):
m_appointment(appointment),
m_interactive(get_interactive()),
m_sound_builder(sound_builder)
{
// ensure notify_init() is called once
// before we start popping up dialogs
static bool m_nn_inited = false;
if (G_UNLIKELY(!m_nn_inited))
{
if(!notify_init("indicator-datetime-service"))
g_critical("libnotify initialization failed");
m_nn_inited = true;
}
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;
/// strftime(3) format string for an alarm's snap decision
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_interactive)
{
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");
/// alarm popup dialog's button to show the active alarm
notify_notification_add_action(m_nn, "show", _("Show"),
on_snap_show, this, nullptr);
/// 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;
}
// Loop the sound *only* if we're prompting the user for a response.
// Otherwise, just play the sound once.
m_sound_builder.set_looping (shown && m_interactive);
m_sound.reset (m_sound_builder());
// 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*, 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*, 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);
}
/***
**** Interactive
***/
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;
}
static bool get_interactive()
{
static bool interactive;
static bool inited = false;
if (G_UNLIKELY(!inited))
{
interactive = get_server_caps().count("actions") != 0;
inited = true;
}
return interactive;
}
/***
****
***/
typedef Popup Self;
const Appointment m_appointment;
const bool m_interactive;
SoundBuilder m_sound_builder;
std::unique_ptr m_sound;
core::Signal m_response;
Response m_response_value = RESPONSE_CLOSE;
NotifyNotification* m_nn = nullptr;
};
/**
*** libnotify -- snap decisions
**/
std::string get_local_filename (const std::string& str)
{
std::string ret;
if (!str.empty())
{
GFile* files[] = { g_file_new_for_path(str.c_str()),
g_file_new_for_uri(str.c_str()) };
for(auto& file : files)
{
if (g_file_is_native(file) && g_file_query_exists(file, nullptr))
{
char* tmp = g_file_get_path(file);
if (tmp != nullptr)
{
ret = tmp;
g_free(tmp);
break;
}
}
}
for(auto& file : files)
g_object_unref(file);
}
return ret;
}
std::string get_alarm_sound(const Appointment& appointment,
const std::shared_ptr& settings)
{
const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
const std::string candidates[] = { appointment.audio_url,
settings->alarm_sound.get(),
FALLBACK };
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& clock,
const std::shared_ptr& settings):
m_clock(clock),
m_settings(settings)
{
}
Snap::~Snap()
{
}
void Snap::operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss)
{
if (!appointment.has_alarms)
{
dismiss(appointment);
return;
}
// create a popup...
SoundBuilder sound_builder;
sound_builder.set_filename(get_alarm_sound(appointment, m_settings));
sound_builder.set_volume(m_settings->alarm_volume.get());
sound_builder.set_clock(m_clock);
sound_builder.set_duration_minutes(m_settings->alarm_duration.get());
auto popup = new Popup(appointment, sound_builder);
// 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