aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--MERGE-REVIEW53
-rw-r--r--data/CMakeLists.txt10
-rw-r--r--data/com.canonical.indicator.datetime.AlarmProperties.xml31
-rw-r--r--data/com.canonical.indicator.datetime.gschema.xml.in (renamed from data/com.canonical.indicator.datetime.gschema.xml)107
-rw-r--r--debian/changelog24
-rw-r--r--debian/control4
-rw-r--r--include/datetime/appointment.h1
-rw-r--r--include/datetime/exporter.h27
-rw-r--r--include/datetime/settings-live.h3
-rw-r--r--include/datetime/settings-shared.h3
-rw-r--r--include/datetime/settings.h3
-rw-r--r--include/datetime/snap.h9
-rw-r--r--src/CMakeLists.txt31
-rw-r--r--src/engine-eds.cpp24
-rw-r--r--src/exporter.cpp249
-rw-r--r--src/main.cpp6
-rw-r--r--src/settings-live.cpp38
-rw-r--r--src/snap.cpp536
-rw-r--r--tests/CMakeLists.txt17
-rw-r--r--tests/manual32
-rw-r--r--tests/manual-test-snap.cpp53
-rw-r--r--tests/test-exporter.cpp95
-rw-r--r--tests/test-locations.cpp1
-rw-r--r--tests/test-menus.cpp2
-rw-r--r--tests/test-settings.cpp30
-rw-r--r--tests/test-snap.cpp212
27 files changed, 1225 insertions, 378 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1390f44..9b4987e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -38,7 +38,7 @@ pkg_check_modules (SERVICE_DEPS REQUIRED
libical>=0.48
libecal-1.2>=3.5
libedataserver-1.2>=3.5
- libcanberra>=0.12
+ gstreamer-1.0>=1.2
libnotify>=0.7.6
url-dispatcher-1>=1
properties-cpp>=0.0.1)
diff --git a/MERGE-REVIEW b/MERGE-REVIEW
index 1a5815c..88f25f6 100644
--- a/MERGE-REVIEW
+++ b/MERGE-REVIEW
@@ -1,44 +1,35 @@
+* '''Checklist for component''': indicator-datetime
+ * '''Component Test Plan''': https://wiki.ubuntu.com/Process/Merges/TestPlan/indicator-datetime
+ * '''Trunk URL''': lp:indicator-datetime
+ * '''Ubuntu Package URL (LP)''': http://launchpad.net/ubuntu/+source/indicator-datetime
This documents the expections that the project has on what both submitters
and reviewers should ensure that they've done for a merge into the project.
-== Submitter Responsibilities ==
+== MP Submission Checklist Template ==
- * Ensure the project compiles and the test suite executes without error
- * Ensure that non-obvious code has comments explaining it
- * If the change affects specific features, please reference the appropriate
- tags in the merge description so reviewers can test appropriately:
- [phone profile], [desktop profile], [alarms]
+'''Note: Please ensure you include the following form filled out and submitted along side your code to the MP ticket.'''
-== Reviewer Responsibilities ==
+ * Are there any related MPs required for this MP to build/function as expected? Please list.
+ * Is your branch in sync with latest trunk? (e.g. bzr pull lp:trunk -> no changes)
+ * Did the code build without warnings?
+ * Did the tests run successfully?
+ * Did you perform an exploratory manual test run of your code change and any related functionality?
+ * If you changed the packaging (debian), did you subscribe the ubuntu-unity team to this MP?
+ * Has your component test plan been executed successfully on emulator, N4?
+ * Please list which manual tests are germane for the reviewer in this MR.
- * Did the Jenkins build compile? Pass? Run unit tests successfully?
- * Are there appropriate tests to cover any new functionality?
- * Do the tag-specific tests pass?
+== MP Review Checklist Template ==
-== Phone Profile Tests ==
+'''Note: Please ensure you include the following form filled out and submitted along side your code to the MP ticket.'''
- * Run tests indicator-datetime/unity8*
+ * Have you checked that the submitter has accurately filled out the submitter checklist and has taken no shortcuts?
+ * Did you run the manual tests listed by the submitter?
+ * Did you do exploratory testing related to the component you own with the MP changeset included?
+ * If new features have been added, are the manual tests sufficient to cover them?
-== Desktop Profile Tests ==
+== MP Landing Checklist Template ==
- * Run tests indicator-datetime/unity7*
+ * Ensure that the checklists have been properly filled out by submitter and all reviewers
-== Alarm Tests ==
-
- * Hardware wakeups for new alarms:
- 1. Create and save an upcoming alarm in ubuntu-clock-app.
- 2. Unplug the phone from any USB connection and put it to sleep.
- 3. Confirm that the alarm sounds on time even if the phone is asleep.
- (Note: if in doubt about sleep you can see in the syslog whether the
- device actually suspended or whether the suspend was aborted)
-
- * Hardware wakeups for edited alarms:
- 1. Edit an alarm that's already passed. (see previous test)
- 2. Unplug the phone from any USB connection and put it to sleep.
- 3. Confirm that the alarm sounds on time even if the phone is asleep.
- (Note: if in doubt about sleep you can see in the syslog whether the
- device actually suspended or whether the suspend was aborted.)
-
-
diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt
index 5d9e545..d66c865 100644
--- a/data/CMakeLists.txt
+++ b/data/CMakeLists.txt
@@ -4,9 +4,15 @@
include (UseGSettings)
set (SCHEMA_NAME "com.canonical.indicator.datetime.gschema.xml")
-set (SCHEMA_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${SCHEMA_NAME}")
-add_schema (${SCHEMA_FILE})
+set (SCHEMA_FILE "${CMAKE_CURRENT_BINARY_DIR}/${SCHEMA_NAME}")
+set (SCHEMA_FILE_IN "${CMAKE_CURRENT_SOURCE_DIR}/${SCHEMA_NAME}.in")
+
+# generate the .xml file using intltool
+set (ENV{LC_ALL} "C")
+execute_process (COMMAND intltool-merge -quiet --xml-style --utf8 --no-translations "${SCHEMA_FILE_IN}" "${SCHEMA_FILE}")
+# let UseGSettings do the rest
+add_schema (${SCHEMA_FILE})
##
## Upstart Job File
diff --git a/data/com.canonical.indicator.datetime.AlarmProperties.xml b/data/com.canonical.indicator.datetime.AlarmProperties.xml
new file mode 100644
index 0000000..d25fa82
--- /dev/null
+++ b/data/com.canonical.indicator.datetime.AlarmProperties.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+ <interface name="com.canonical.indicator.datetime.AlarmProperties">
+
+ <property name="DefaultSound" type="s" access="readwrite">
+ <doc:doc>
+ <doc:description>
+ <doc:para>The default alarm sound's filename.</doc:para>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ <property name="DefaultVolume" type="i" access="readwrite">
+ <doc:doc>
+ <doc:description>
+ <doc:para>How loudly to play alarm sounds. [Range: 1-100]</doc:para>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ <property name="Duration" type="i" access="readwrite">
+ <doc:doc>
+ <doc:description>
+ <doc:para>How long an alarm's sound should be looped.</doc:para>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ </interface>
+</node>
diff --git a/data/com.canonical.indicator.datetime.gschema.xml b/data/com.canonical.indicator.datetime.gschema.xml.in
index 1a5922c..62b42c1 100644
--- a/data/com.canonical.indicator.datetime.gschema.xml
+++ b/data/com.canonical.indicator.datetime.gschema.xml.in
@@ -8,120 +8,143 @@
<schema id="com.canonical.indicator.datetime" path="/com/canonical/indicator/datetime/" gettext-domain="indicator-datetime">
<key name="show-clock" type="b">
<default>true</default>
- <summary>Show the clock in the panel</summary>
- <description>
+ <_summary>Show the clock in the panel</_summary>
+ <_description>
Controls whether the clock indicator appears in the panel or not.
- </description>
+ </_description>
</key>
<key name="time-format" enum="time-enum">
<default>'locale-default'</default>
- <summary>What the time format should be</summary>
- <description>
+ <_summary>What the time format should be</_summary>
+ <_description>
Controls the time format that is displayed in the indicator. For almost
all users this should be the default for their locale. If you think the
setting is wrong for your locale please join or talk to the translation
team for your language. If you just want something different you can
adjust this to be either 12 or 24 time. Or, you can use a custom format
string and set the custom-time-format setting.
- </description>
+ </_description>
</key>
<key name="custom-time-format" type="s">
<default>"%l:%M %p"</default>
- <summary>The format string passed to strftime</summary>
- <description>
+ <_summary>The format string passed to strftime</_summary>
+ <_description>
The format of the time and/or date that is visible on the panel when using
the indicator. For most users this will be a set of predefined values as
determined by the configuration utility, but advanced users can change it
to anything strftime can accept. Look at the man page on strftime for
more information.
- </description>
+ </_description>
</key>
<key name="show-seconds" type="b">
<default>false</default>
- <summary>Show the number of seconds in the indicator</summary>
- <description>
+ <_summary>Show the number of seconds in the indicator</_summary>
+ <_description>
Makes the datetime indicator show the number of seconds in the indicator.
It's important to note that this will cause additional battery drain as
the time will update 60 times as often, so it is not recommended. Also,
this setting will be ignored if the time-format value is set to custom.
- </description>
+ </_description>
</key>
<key name="show-day" type="b">
<default>false</default>
- <summary>Show the day of the week in the indicator</summary>
- <description>
+ <_summary>Show the day of the week in the indicator</_summary>
+ <_description>
Puts the day of the week on the panel along with the time and/or date
depending on settings. This setting will be ignored if the time-format
value is set to custom.
- </description>
+ </_description>
</key>
<key name="show-date" type="b">
<default>false</default>
- <summary>Show the month and date in the indicator</summary>
- <description>
+ <_summary>Show the month and date in the indicator</_summary>
+ <_description>
Puts the month and the date in the panel along with the time and/or day
of the week depending on settings. This setting will be ignored if the
time-format value is set to custom.
- </description>
+ </_description>
</key>
<key name="show-year" type="b">
<default>false</default>
- <summary>Show the year in the indicator</summary>
- <description>
+ <_summary>Show the year in the indicator</_summary>
+ <_description>
Puts the year in the panel along with the month and the date.
This setting will be ignored if either the time-format value is set to custom
or if show-date is set to false.
- </description>
+ </_description>
</key>
<key name="show-calendar" type="b">
<default>true</default>
- <summary>Show the monthly calendar in the indicator</summary>
- <description>
+ <_summary>Show the monthly calendar in the indicator</_summary>
+ <_description>
Puts the monthly calendar in indicator-datetime's menu.
- </description>
+ </_description>
</key>
<key name="show-week-numbers" type="b">
<default>false</default>
- <summary>Show week numbers in calendar</summary>
- <description>
+ <_summary>Show week numbers in calendar</_summary>
+ <_description>
Shows the week numbers in the monthly calendar in indicator-datetime's menu.
- </description>
+ </_description>
</key>
<key name="show-events" type="b">
<default>true</default>
- <summary>Show events in the indicator</summary>
- <description>
+ <_summary>Show events in the indicator</_summary>
+ <_description>
Shows events from Evolution in indicator-datetime's menu.
- </description>
+ </_description>
</key>
<key name="show-auto-detected-location" type="b">
<default>false</default>
- <summary>Show the auto-detected location in the indicator</summary>
- <description>
+ <_summary>Show the auto-detected location in the indicator</_summary>
+ <_description>
Shows your current location (determined from geoclue and /etc/timezone) in indicator-datetime's menu.
- </description>
+ </_description>
</key>
<key name="show-locations" type="b">
<default>false</default>
- <summary>Show locations in the indicator</summary>
- <description>
+ <_summary>Show locations in the indicator</_summary>
+ <_description>
Shows custom defined locations in indicator-datetime's menu.
- </description>
+ </_description>
</key>
<key name="locations" type="as">
<default>['UTC']</default>
- <summary>A List of locations</summary>
- <description>
+ <_summary>A List of locations</_summary>
+ <_description>
Adds the list of locations the user has configured to display in the
indicator-datetime menu.
- </description>
+ </_description>
</key>
<key name="timezone-name" type="s">
<default>''</default>
- <summary>The name of the current timezone</summary>
- <description>
+ <_summary>The name of the current timezone</_summary>
+ <_description>
Some timezones can be known by many different cities or names. This setting describes how the current zone prefers to be named. Format is "TIMEZONE NAME" (e.g. "America/New_York Boston" to name the New_York zone Boston).
- </description>
+ </_description>
+ </key>
+ <key name="alarm-default-sound" type="s">
+ <default>'/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg'</default>
+ <_summary>The alarm's default sound file.</_summary>
+ <_description>
+ If an alarm doesn't specify its own sound file, this file will be used as the fallback sound.
+ </_description>
+ </key>
+ <key name="alarm-default-volume" type="u">
+ <range min="1" max="100"/>
+ <default>50</default>
+ <_summary>The alarm's default volume level.</_summary>
+ <_description>
+ The volume at which alarms will be played.
+ </_description>
+ </key>
+ <key name="alarm-duration-minutes" type="u">
+ <range min="1" max="60"/>
+ <default>30</default>
+ <_summary>The alarm's duration.</_summary>
+ <_description>
+ How long the alarm's sound will be looped if its snap decision is not dismissed by the user.
+ </_description>
</key>
</schema>
</schemalist>
diff --git a/debian/changelog b/debian/changelog
index c77c4bb..21e1db3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,27 @@
+indicator-datetime (13.10.0+14.10.20140716-0ubuntu1) utopic; urgency=low
+
+ [ Charles Kerr ]
+ * Set a x-canonical-snap-decisions-timeout hint for the alarm snap
+ decisions. (LP: #1324580)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Wed, 16 Jul 2014 19:23:15 +0000
+
+indicator-datetime (13.10.0+14.10.20140714.1-0ubuntu1) utopic; urgency=low
+
+ [ Charles Kerr ]
+ * Use GStreamer's API directly to play sound instead of using
+ libcanberra. (LP: #1283065)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 14 Jul 2014 13:40:44 +0000
+
+indicator-datetime (13.10.0+14.10.20140701-0ubuntu1) utopic; urgency=low
+
+ [ Charles Kerr ]
+ * Add the ability to have per-alarm custom sounds. (LP: #1318997)
+ * Expose the new alarm settings as DBus properties. (LP: #1318997)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Tue, 01 Jul 2014 09:37:06 +0000
+
indicator-datetime (13.10.0+14.10.20140611-0ubuntu3) utopic; urgency=medium
* Migrading to Platform V2
diff --git a/debian/control b/debian/control
index 6699f31..9e7133e 100644
--- a/debian/control
+++ b/debian/control
@@ -12,11 +12,11 @@ Build-Depends: cmake,
python3-dbusmock,
debhelper (>= 9),
dh-translations,
- language-pack-en-base,
+ language-pack-touch-en | language-pack-en-base,
libgtest-dev,
libglib2.0-dev (>= 2.35.4),
libnotify-dev (>= 0.7.6),
- libcanberra-dev,
+ libgstreamer1.0-dev,
libecal1.2-dev (>= 3.5),
libical-dev (>= 1.0),
libedataserver1.2-dev (>= 3.5),
diff --git a/include/datetime/appointment.h b/include/datetime/appointment.h
index 4778293..2e406a2 100644
--- a/include/datetime/appointment.h
+++ b/include/datetime/appointment.h
@@ -39,6 +39,7 @@ public:
std::string summary;
std::string url;
std::string uid;
+ std::string audio_url;
bool has_alarms = false;
DateTime begin;
DateTime end;
diff --git a/include/datetime/exporter.h b/include/datetime/exporter.h
index c228cc1..dd57263 100644
--- a/include/datetime/exporter.h
+++ b/include/datetime/exporter.h
@@ -22,11 +22,10 @@
#include <datetime/actions.h>
#include <datetime/menu.h>
+#include <datetime/settings.h>
#include <core/signal.h>
-#include <gio/gio.h> // GActionGroup
-
#include <memory> // std::shared_ptr
#include <vector>
@@ -40,31 +39,21 @@ namespace datetime {
class Exporter
{
public:
- Exporter() =default;
+ Exporter(const std::shared_ptr<Settings>&);
~Exporter();
- core::Signal<> name_lost;
+ core::Signal<>& name_lost();
void publish(const std::shared_ptr<Actions>& actions,
const std::vector<std::shared_ptr<Menu>>& menus);
private:
- static void on_bus_acquired(GDBusConnection*, const gchar *name, gpointer gthis);
- void on_bus_acquired(GDBusConnection*, const gchar *name);
-
- static void on_name_lost(GDBusConnection*, const gchar *name, gpointer gthis);
- void on_name_lost(GDBusConnection*, const gchar *name);
-
- std::set<guint> m_exported_menu_ids;
- guint m_own_id = 0;
- guint m_exported_actions_id = 0;
- GDBusConnection * m_dbus_connection = nullptr;
- std::shared_ptr<Actions> m_actions;
- std::vector<std::shared_ptr<Menu>> m_menus;
+ class Impl;
+ std::unique_ptr<Impl> p;
- // we've got raw pointers and gsignal tags in here, so disable copying
- Exporter(const Exporter&) =delete;
- Exporter& operator=(const Exporter&) =delete;
+ // disable copying
+ Exporter(const Exporter&) =delete;
+ Exporter& operator=(const Exporter&) =delete;
};
} // namespace datetime
diff --git a/include/datetime/settings-live.h b/include/datetime/settings-live.h
index 202c998..4db2d40 100644
--- a/include/datetime/settings-live.h
+++ b/include/datetime/settings-live.h
@@ -55,6 +55,9 @@ private:
void update_show_year();
void update_time_format_mode();
void update_timezone_name();
+ void update_alarm_sound();
+ void update_alarm_volume();
+ void update_alarm_duration();
GSettings* m_settings;
diff --git a/include/datetime/settings-shared.h b/include/datetime/settings-shared.h
index 17a8ef0..23d2e1c 100644
--- a/include/datetime/settings-shared.h
+++ b/include/datetime/settings-shared.h
@@ -45,5 +45,8 @@ TimeFormatMode;
#define SETTINGS_SHOW_DETECTED_S "show-auto-detected-location"
#define SETTINGS_LOCATIONS_S "locations"
#define SETTINGS_TIMEZONE_NAME_S "timezone-name"
+#define SETTINGS_ALARM_SOUND_S "alarm-default-sound"
+#define SETTINGS_ALARM_VOLUME_S "alarm-default-volume"
+#define SETTINGS_ALARM_DURATION_S "alarm-duration-minutes"
#endif // INDICATOR_DATETIME_SETTINGS_SHARED
diff --git a/include/datetime/settings.h b/include/datetime/settings.h
index ce234d9..e5f885e 100644
--- a/include/datetime/settings.h
+++ b/include/datetime/settings.h
@@ -56,6 +56,9 @@ public:
core::Property<bool> show_year;
core::Property<TimeFormatMode> time_format_mode;
core::Property<std::string> timezone_name;
+ core::Property<std::string> alarm_sound;
+ core::Property<unsigned int> alarm_volume;
+ core::Property<unsigned int> alarm_duration;
};
} // namespace datetime
diff --git a/include/datetime/snap.h b/include/datetime/snap.h
index a493772..9b45b3f 100644
--- a/include/datetime/snap.h
+++ b/include/datetime/snap.h
@@ -21,6 +21,8 @@
#define INDICATOR_DATETIME_SNAP_H
#include <datetime/appointment.h>
+#include <datetime/clock.h>
+#include <datetime/settings.h>
#include <memory>
#include <functional>
@@ -35,13 +37,18 @@ namespace datetime {
class Snap
{
public:
- Snap();
+ Snap(const std::shared_ptr<Clock>& clock,
+ const std::shared_ptr<const Settings>& settings);
virtual ~Snap();
typedef std::function<void(const Appointment&)> appointment_func;
void operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss);
+
+private:
+ const std::shared_ptr<Clock> m_clock;
+ const std::shared_ptr<const Settings> m_settings;
};
} // namespace datetime
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ffa1523..af09c71 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,13 +1,13 @@
set (SERVICE_LIB "indicatordatetimeservice")
set (SERVICE_EXEC "indicator-datetime-service")
-SET (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -g ${CXX_WARNING_ARGS} ${GCOV_FLAGS}")
-SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g ${CXX_WARNING_ARGS} ${GCOV_FLAGS}")
-
add_definitions (-DTIMEZONE_FILE="/etc/timezone"
-DG_LOG_DOMAIN="Indicator-Datetime")
-set (SERVICE_SOURCES
+# handwritten sources
+set (SERVICE_C_SOURCES
+ utils.c)
+set (SERVICE_CXX_SOURCES
actions.cpp
actions-live.cpp
alarm-queue-simple.cpp
@@ -32,16 +32,33 @@ set (SERVICE_SOURCES
timezones-live.cpp
utils.c
wakeup-timer-mainloop.cpp)
-
if (HAVE_UBUNTU_HW_ALARM_H)
- set (SERVICE_SOURCES ${SERVICE_SOURCES} wakeup-timer-uha.cpp)
+ set (SERVICE_CXX_SOURCES ${SERVICE_CXX_SOURCES} wakeup-timer-uha.cpp)
endif ()
-add_library (${SERVICE_LIB} STATIC ${SERVICE_SOURCES})
+# generated sources
+include (GdbusCodegen)
+set(SERVICE_GENERATED_SOURCES)
+add_gdbus_codegen(SERVICE_GENERATED_SOURCES dbus-alarm-properties
+ com.canonical.indicator
+ ${CMAKE_SOURCE_DIR}/data/com.canonical.indicator.datetime.AlarmProperties.xml)
+
+# add warnings/coverage info on handwritten files
+# but not the autogenerated ones...
+set_source_files_properties(${SERVICE_CXX_SOURCES}
+ PROPERTIES COMPILE_FLAGS "${CXX_WARNING_ARGS} ${GCOV_FLAGS} -g -std=c++11")
+set_source_files_properties(${SERVICE_C_SOURCES}
+ PROPERTIES COMPILE_FLAGS "${CXX_WARNING_ARGS} ${GCOV_FLAGS} -g -std=c99")
+
+# add the bin dir to our include path so our code can find the generated header files
+include_directories (${CMAKE_CURRENT_BINARY_DIR})
+
+add_library (${SERVICE_LIB} STATIC ${SERVICE_C_SOURCES} ${SERVICE_CXX_SOURCES} ${SERVICE_GENERATED_SOURCES})
include_directories (${CMAKE_SOURCE_DIR})
link_directories (${SERVICE_DEPS_LIBRARY_DIRS})
add_executable (${SERVICE_EXEC} main.cpp)
+set_source_files_properties(${SERVICE_SOURCES} main.cpp PROPERTIES COMPILE_FLAGS "${CXX_WARNING_ARGS} -g -std=c++11")
target_link_libraries (${SERVICE_EXEC} ${SERVICE_LIB} ${SERVICE_DEPS_LIBRARIES} ${GCOV_LIBS})
install (TARGETS ${SERVICE_EXEC} RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_PKGLIBEXECDIR})
diff --git a/src/engine-eds.cpp b/src/engine-eds.cpp
index 1949193..58be0c4 100644
--- a/src/engine-eds.cpp
+++ b/src/engine-eds.cpp
@@ -41,8 +41,7 @@ class EdsEngine::Impl
{
public:
- Impl(EdsEngine& owner):
- m_owner(owner),
+ Impl():
m_cancellable(g_cancellable_new())
{
e_source_registry_new(m_cancellable, on_source_registry_ready, this);
@@ -443,8 +442,9 @@ private:
appointment.color = subtask->color;
appointment.uid = uid;
- // if the component has display alarms that have a url,
- // use the first one as our Appointment.url
+ // Look through all of this component's alarms
+ // for DISPLAY or AUDIO url attachments.
+ // If we find any, use them for appointment.url and audio_sound
auto alarm_uids = e_cal_component_get_alarm_uids(component);
appointment.has_alarms = alarm_uids != nullptr;
for(auto walk=alarm_uids; appointment.url.empty() && walk!=nullptr; walk=walk->next)
@@ -453,7 +453,7 @@ private:
ECalComponentAlarmAction action;
e_cal_component_alarm_get_action(alarm, &action);
- if (action == E_CAL_COMPONENT_ALARM_DISPLAY)
+ if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) || (action == E_CAL_COMPONENT_ALARM_AUDIO))
{
icalattach* attach = nullptr;
e_cal_component_alarm_get_attach(alarm, &attach);
@@ -463,7 +463,16 @@ private:
{
const char* url = icalattach_get_url(attach);
if (url != nullptr)
- appointment.url = url;
+ {
+ if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) && appointment.url.empty())
+ {
+ appointment.url = url;
+ }
+ else if ((action == E_CAL_COMPONENT_ALARM_AUDIO) && appointment.audio_url.empty())
+ {
+ appointment.audio_url = url;
+ }
+ }
}
icalattach_unref(attach);
@@ -482,7 +491,6 @@ private:
return G_SOURCE_CONTINUE;
}
- EdsEngine& m_owner;
core::Signal<> m_changed;
std::set<ESource*> m_sources;
std::map<ESource*,ECalClient*> m_clients;
@@ -498,7 +506,7 @@ private:
***/
EdsEngine::EdsEngine():
- p(new Impl(*this))
+ p(new Impl())
{
}
diff --git a/src/exporter.cpp b/src/exporter.cpp
index ccd6e5c..e2b60f2 100644
--- a/src/exporter.cpp
+++ b/src/exporter.cpp
@@ -20,6 +20,8 @@
#include <datetime/dbus-shared.h>
#include <datetime/exporter.h>
+#include "dbus-alarm-properties.h"
+
#include <glib/gi18n.h>
#include <gio/gio.h>
@@ -31,108 +33,223 @@ namespace datetime {
****
***/
-Exporter::~Exporter()
+class Exporter::Impl
{
- if (m_dbus_connection != nullptr)
+public:
+
+ Impl(const std::shared_ptr<Settings>& settings):
+ m_settings(settings),
+ m_alarm_props(datetime_alarm_properties_skeleton_new())
+ {
+ alarm_properties_init();
+ }
+
+ ~Impl()
{
- for(auto& id : m_exported_menu_ids)
- g_dbus_connection_unexport_menu_model(m_dbus_connection, id);
+ if (m_bus != nullptr)
+ {
+ for(auto& id : m_exported_menu_ids)
+ g_dbus_connection_unexport_menu_model(m_bus, id);
- if (m_exported_actions_id)
- g_dbus_connection_unexport_action_group(m_dbus_connection, m_exported_actions_id);
+ if (m_exported_actions_id)
+ g_dbus_connection_unexport_action_group(m_bus, m_exported_actions_id);
+ }
+
+ g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(m_alarm_props));
+ g_clear_object(&m_alarm_props);
+
+ if (m_own_id)
+ g_bus_unown_name(m_own_id);
+
+ g_clear_object(&m_bus);
}
- if (m_own_id)
- g_bus_unown_name(m_own_id);
+ core::Signal<> name_lost;
- g_clear_object(&m_dbus_connection);
-}
+ void publish(const std::shared_ptr<Actions>& actions,
+ const std::vector<std::shared_ptr<Menu>>& menus)
+ {
+ m_actions = actions;
+ m_menus = menus;
+ m_own_id = g_bus_own_name(G_BUS_TYPE_SESSION,
+ BUS_NAME,
+ G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT,
+ on_bus_acquired,
+ nullptr,
+ on_name_lost,
+ this,
+ nullptr);
+ }
-/***
-****
-***/
+private:
-void
-Exporter::on_bus_acquired(GDBusConnection* connection, const gchar* name, gpointer gthis)
-{
- g_debug("bus acquired: %s", name);
- static_cast<Exporter*>(gthis)->on_bus_acquired(connection, name);
-}
+ /***
+ ****
+ ***/
-void
-Exporter::on_bus_acquired(GDBusConnection* connection, const gchar* /*name*/)
-{
- m_dbus_connection = static_cast<GDBusConnection*>(g_object_ref(G_OBJECT(connection)));
-
- // export the actions
- GError * error = nullptr;
- const auto id = g_dbus_connection_export_action_group(m_dbus_connection,
- BUS_PATH,
- m_actions->action_group(),
- &error);
- if (id)
+ static void
+ on_gobject_notify_string(GObject* o, GParamSpec* pspec, gpointer p)
+ {
+ gchar* val = nullptr;
+ g_object_get (o, pspec->name, &val, nullptr);
+ static_cast<core::Property<std::string>*>(p)->set(val);
+ g_free(val);
+ }
+ void bind_string_property(gpointer o, const char* propname,
+ core::Property<std::string>& p)
+ {
+ // initialize the GObject property from the Settings
+ g_object_set(o, propname, p.get().c_str(), nullptr);
+
+ // when the GObject changes, update the Settings
+ const std::string notify_propname = std::string("notify::") + propname;
+ g_signal_connect(o, notify_propname.c_str(),
+ G_CALLBACK(on_gobject_notify_string), &p);
+
+ // when the Settings changes, update the GObject
+ p.changed().connect([o, propname](const std::string& val){
+ g_object_set(o, propname, val.c_str(), nullptr);
+ });
+ }
+
+
+ static void
+ on_gobject_notify_uint(GObject* o, GParamSpec* pspec, gpointer p)
+ {
+ uint val = 0;
+ g_object_get (o, pspec->name, &val, nullptr);
+ static_cast<core::Property<unsigned int>*>(p)->set(val);
+ }
+ void bind_uint_property(gpointer o,
+ const char* propname,
+ core::Property<unsigned int>& p)
{
- m_exported_actions_id = id;
+ // initialize the GObject property from the Settings
+ g_object_set(o, propname, p.get(), nullptr);
+
+ // when the GObject changes, update the Settings
+ const std::string notify_propname = std::string("notify::") + propname;
+ g_signal_connect(o, notify_propname.c_str(),
+ G_CALLBACK(on_gobject_notify_uint), &p);
+
+ // when the Settings changes, update the GObject
+ p.changed().connect([o, propname](unsigned int val){
+ g_object_set(o, propname, val, nullptr);
+ });
}
- else
+
+
+ void alarm_properties_init()
{
- g_warning("cannot export action group: %s", error->message);
- g_clear_error(&error);
+ bind_uint_property(m_alarm_props, "duration", m_settings->alarm_duration);
+ bind_uint_property(m_alarm_props, "default-volume", m_settings->alarm_volume);
+ bind_string_property(m_alarm_props, "default-sound", m_settings->alarm_sound);
}
- // export the menus
- for(auto& menu : m_menus)
+ /***
+ ****
+ ***/
+
+ static void on_bus_acquired(GDBusConnection* connection,
+ const gchar* name,
+ gpointer gthis)
{
- const auto path = std::string(BUS_PATH) + "/" + menu->name();
- const auto id = g_dbus_connection_export_menu_model(m_dbus_connection, path.c_str(), menu->menu_model(), &error);
+ g_debug("bus acquired: %s", name);
+ static_cast<Impl*>(gthis)->on_bus_acquired(connection, name);
+ }
+
+ void on_bus_acquired(GDBusConnection* bus, const gchar* /*name*/)
+ {
+ m_bus = static_cast<GDBusConnection*>(g_object_ref(G_OBJECT(bus)));
+
+ // export the alarm properties
+ GError * error = nullptr;
+ g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(m_alarm_props),
+ m_bus,
+ BUS_PATH"/AlarmProperties",
+ &error);
+
+ // export the actions
+ const auto id = g_dbus_connection_export_action_group(m_bus,
+ BUS_PATH,
+ m_actions->action_group(),
+ &error);
if (id)
{
- m_exported_menu_ids.insert(id);
+ m_exported_actions_id = id;
}
else
{
- if (error != nullptr)
- g_warning("cannot export %s menu: %s", menu->name().c_str(), error->message);
+ g_warning("cannot export action group: %s", error->message);
g_clear_error(&error);
}
+
+ // export the menus
+ for(auto& menu : m_menus)
+ {
+ const auto path = std::string(BUS_PATH) + "/" + menu->name();
+ const auto id = g_dbus_connection_export_menu_model(m_bus, path.c_str(), menu->menu_model(), &error);
+ if (id)
+ {
+ m_exported_menu_ids.insert(id);
+ }
+ else
+ {
+ if (error != nullptr)
+ g_warning("cannot export %s menu: %s", menu->name().c_str(), error->message);
+ g_clear_error(&error);
+ }
+ }
}
-}
+
+ /***
+ ****
+ ***/
+
+ static void on_name_lost(GDBusConnection*, const gchar* name, gpointer gthis)
+ {
+ g_debug("name lost: %s", name);
+ static_cast<Impl*>(gthis)->name_lost();
+ }
+
+ /***
+ ****
+ ***/
+
+ std::shared_ptr<Settings> m_settings;
+ std::set<guint> m_exported_menu_ids;
+ guint m_own_id = 0;
+ guint m_exported_actions_id = 0;
+ GDBusConnection* m_bus = nullptr;
+ std::shared_ptr<Actions> m_actions;
+ std::vector<std::shared_ptr<Menu>> m_menus;
+ DatetimeAlarmProperties* m_alarm_props = nullptr;
+};
+
/***
****
***/
-void
-Exporter::on_name_lost(GDBusConnection* connection, const gchar* name, gpointer gthis)
+Exporter::Exporter(const std::shared_ptr<Settings>& settings):
+ p(new Impl(settings))
{
- g_debug("name lost: %s", name);
- static_cast<Exporter*>(gthis)->on_name_lost(connection, name);
}
-void
-Exporter::on_name_lost(GDBusConnection* /*connection*/, const gchar* /*name*/)
+
+Exporter::~Exporter()
{
- name_lost();
}
-/***
-****
-***/
+core::Signal<>& Exporter::name_lost()
+{
+ return p->name_lost;
+}
-void
-Exporter::publish(const std::shared_ptr<Actions>& actions,
- const std::vector<std::shared_ptr<Menu>>& menus)
+void Exporter::publish(const std::shared_ptr<Actions>& actions,
+ const std::vector<std::shared_ptr<Menu>>& menus)
{
- m_actions = actions;
- m_menus = menus;
- m_own_id = g_bus_own_name(G_BUS_TYPE_SESSION,
- BUS_NAME,
- G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT,
- on_bus_acquired,
- nullptr,
- on_name_lost,
- this,
- nullptr);
+ p->publish(actions, menus);
}
/***
diff --git a/src/main.cpp b/src/main.cpp
index 079fe35..cc81cd7 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -141,7 +141,7 @@ main(int /*argc*/, char** /*argv*/)
MenuFactory factory(actions, state);
// set up the snap decisions
- Snap snap;
+ Snap snap (state->clock, state->settings);
auto alarm_queue = create_simple_alarm_queue(state->clock, engine, timezone);
alarm_queue->alarm_reached().connect([&snap](const Appointment& appt){
auto snap_show = [](const Appointment& a){
@@ -163,8 +163,8 @@ main(int /*argc*/, char** /*argv*/)
// export them & run until we lose the busname
auto loop = g_main_loop_new(nullptr, false);
- Exporter exporter;
- exporter.name_lost.connect([loop](){
+ Exporter exporter(state->settings);
+ exporter.name_lost().connect([loop](){
g_message("%s exiting; failed/lost bus ownership", GETTEXT_PACKAGE);
g_main_loop_quit(loop);
});
diff --git a/src/settings-live.cpp b/src/settings-live.cpp
index 2305c93..71bbd96 100644
--- a/src/settings-live.cpp
+++ b/src/settings-live.cpp
@@ -52,6 +52,9 @@ LiveSettings::LiveSettings():
update_show_year();
update_time_format_mode();
update_timezone_name();
+ update_alarm_sound();
+ update_alarm_volume();
+ update_alarm_duration();
// now listen for clients to change the properties s.t. we can sync update GSettings
@@ -115,6 +118,18 @@ LiveSettings::LiveSettings():
timezone_name.changed().connect([this](const std::string& value){
g_settings_set_string(m_settings, SETTINGS_TIMEZONE_NAME_S, value.c_str());
});
+
+ alarm_sound.changed().connect([this](const std::string& value){
+ g_settings_set_string(m_settings, SETTINGS_ALARM_SOUND_S, value.c_str());
+ });
+
+ alarm_volume.changed().connect([this](unsigned int value){
+ g_settings_set_uint(m_settings, SETTINGS_ALARM_VOLUME_S, value);
+ });
+
+ alarm_duration.changed().connect([this](unsigned int value){
+ g_settings_set_uint(m_settings, SETTINGS_ALARM_DURATION_S, value);
+ });
}
/***
@@ -205,6 +220,23 @@ void LiveSettings::update_timezone_name()
g_free(val);
}
+void LiveSettings::update_alarm_sound()
+{
+ auto val = g_settings_get_string(m_settings, SETTINGS_ALARM_SOUND_S);
+ alarm_sound.set(val);
+ g_free(val);
+}
+
+void LiveSettings::update_alarm_volume()
+{
+ alarm_volume.set(g_settings_get_uint(m_settings, SETTINGS_ALARM_VOLUME_S));
+}
+
+void LiveSettings::update_alarm_duration()
+{
+ alarm_duration.set(g_settings_get_uint(m_settings, SETTINGS_ALARM_DURATION_S));
+}
+
/***
****
***/
@@ -246,6 +278,12 @@ void LiveSettings::update_key(const std::string& key)
update_show_detected_locations();
else if (key == SETTINGS_TIMEZONE_NAME_S)
update_timezone_name();
+ else if (key == SETTINGS_ALARM_SOUND_S)
+ update_alarm_sound();
+ else if (key == SETTINGS_ALARM_VOLUME_S)
+ update_alarm_volume();
+ else if (key == SETTINGS_ALARM_DURATION_S)
+ update_alarm_duration();
}
/***
diff --git a/src/snap.cpp b/src/snap.cpp
index a087a75..45eb14e 100644
--- a/src/snap.cpp
+++ b/src/snap.cpp
@@ -21,17 +21,19 @@
#include <datetime/formatter.h>
#include <datetime/snap.h>
-#include <canberra.h>
+#include <core/signal.h>
+
+#include <gst/gst.h>
#include <libnotify/notify.h>
#include <glib/gi18n.h>
#include <glib.h>
+#include <chrono>
+#include <mutex> // std::call_once()
#include <set>
#include <string>
-#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"
-
namespace unity {
namespace indicator {
namespace datetime {
@@ -43,266 +45,434 @@ namespace datetime {
namespace
{
-/**
-*** libcanberra -- play sounds
-**/
-
-// arbitrary number, but we need a consistent id for play/cancel
-const int32_t alarm_ca_id = 1;
-
-ca_context *c_context = nullptr;
-guint timeout_tag = 0;
-
-ca_context* get_ca_context()
+/**
+ * Plays a sound, possibly looping.
+ */
+class Sound
{
- if (G_UNLIKELY(c_context == nullptr))
+ typedef Sound Self;
+
+public:
+
+ Sound(const std::shared_ptr<Clock>& clock,
+ const std::string& uri,
+ unsigned int volume,
+ unsigned int duration_minutes,
+ bool loop):
+ m_clock(clock),
+ m_uri(uri),
+ m_volume(volume),
+ m_loop(loop),
+ m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, (int)duration_minutes, 0.0))
{
- int rv;
-
- if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)
+ // init GST once
+ static std::once_flag once;
+ std::call_once(once, [](){
+ GError* error = nullptr;
+ gst_init_check (nullptr, nullptr, &error);
+ if (error)
+ {
+ g_critical("Unable to play alarm sound: %s", error->message);
+ g_error_free(error);
+ }
+ });
+
+ if (m_loop)
{
- g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));
- c_context = nullptr;
+ g_debug("Looping '%s' until cutoff time %s",
+ m_uri.c_str(),
+ m_loop_end_time.format("%F %T").c_str());
+ }
+ else
+ {
+ g_debug("Playing '%s' once", m_uri.c_str());
}
- }
-
- return c_context;
-}
-void play_alarm_sound();
+ m_play = gst_element_factory_make("playbin", "play");
-gboolean play_alarm_sound_idle (gpointer)
-{
- timeout_tag = 0;
- play_alarm_sound();
- return G_SOURCE_REMOVE;
-}
+ auto bus = gst_pipeline_get_bus(GST_PIPELINE(m_play));
+ m_watch_source = gst_bus_add_watch(bus, bus_callback, this);
+ gst_object_unref(bus);
-void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/)
-{
- // wait one second, then play it again
- if ((rv == CA_SUCCESS) && (timeout_tag == 0))
- timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr);
-}
+ play();
+ }
-void play_alarm_sound()
-{
- const gchar* filename = ALARM_SOUND_FILENAME;
- auto context = get_ca_context();
- g_return_if_fail(context != nullptr);
+ ~Sound()
+ {
+ stop();
- ca_proplist* props = nullptr;
- ca_proplist_create(&props);
- ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
+ g_source_remove(m_watch_source);
- const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr);
- if (rv != CA_SUCCESS)
- g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv));
+ if (m_play != nullptr)
+ {
+ gst_element_set_state (m_play, GST_STATE_NULL);
+ g_clear_pointer (&m_play, gst_object_unref);
+ }
+ }
- g_clear_pointer(&props, ca_proplist_destroy);
-}
+private:
-void stop_alarm_sound()
-{
- auto context = get_ca_context();
- if (context != nullptr)
+ void stop()
{
- const auto rv = ca_context_cancel(context, alarm_ca_id);
- if (rv != CA_SUCCESS)
- g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
+ if (m_play != nullptr)
+ {
+ gst_element_set_state (m_play, GST_STATE_PAUSED);
+ }
}
- if (timeout_tag != 0)
+ void play()
{
- g_source_remove(timeout_tag);
- timeout_tag = 0;
- }
-}
+ g_return_if_fail(m_play != nullptr);
-/**
-*** libnotify -- snap decisions
-**/
+ g_object_set(G_OBJECT (m_play), "uri", m_uri.c_str(),
+ "volume", get_volume(),
+ nullptr);
+ gst_element_set_state (m_play, GST_STATE_PLAYING);
+ }
-void first_time_init()
-{
- static bool inited = false;
+ // convert settings range [1..100] to gst playbin's range is [0...1.0]
+ gdouble get_volume() const
+ {
+ constexpr int in_range_lo = 1;
+ constexpr int in_range_hi = 100;
+ const double in = CLAMP(m_volume, in_range_lo, in_range_hi);
+ const double pct = (in - in_range_lo) / (in_range_hi - in_range_lo);
+
+ constexpr double out_range_lo = 0.0;
+ constexpr double out_range_hi = 1.0;
+ return out_range_lo + (pct * (out_range_hi - out_range_lo));
+ }
- if (G_UNLIKELY(!inited))
+ static gboolean bus_callback(GstBus*, GstMessage* msg, gpointer gself)
{
- inited = true;
+ auto self = static_cast<Sound*>(gself);
+
+ if ((GST_MESSAGE_TYPE(msg) == GST_MESSAGE_EOS) &&
+ (self->m_loop) &&
+ (self->m_clock->localtime() < self->m_loop_end_time))
+ {
+ gst_element_seek(self->m_play,
+ 1.0,
+ GST_FORMAT_TIME,
+ GST_SEEK_FLAG_FLUSH,
+ GST_SEEK_TYPE_SET,
+ 0,
+ GST_SEEK_TYPE_NONE,
+ (gint64)GST_CLOCK_TIME_NONE);
+ }
- if(!notify_init("indicator-datetime-service"))
- g_critical("libnotify initialization failed");
+ return G_SOURCE_CONTINUE; // keep listening
}
-}
-struct SnapData
-{
- Snap::appointment_func show;
- Snap::appointment_func dismiss;
- Appointment appointment;
+ /***
+ ****
+ ***/
+
+ const std::shared_ptr<Clock> m_clock;
+ const std::string m_uri;
+ const unsigned int m_volume;
+ const bool m_loop;
+ const DateTime m_loop_end_time;
+ guint m_watch_source = 0;
+ GstElement* m_play = nullptr;
};
-void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata)
+class SoundBuilder
{
- stop_alarm_sound();
- auto data = static_cast<SnapData*>(gdata);
- data->show(data->appointment);
-}
-
-void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata)
-{
- stop_alarm_sound();
- auto data = static_cast<SnapData*>(gdata);
- data->dismiss(data->appointment);
-}
+public:
+ void set_clock(const std::shared_ptr<Clock>& c) {m_clock = c;}
+ void set_uri(const std::string& uri) {m_uri = uri;}
+ void set_volume(const unsigned int v) {m_volume = v;}
+ void set_duration_minutes(unsigned int i) {m_duration_minutes=i;}
+ unsigned int duration_minutes() const {return m_duration_minutes;}
+ void set_looping(bool b) {m_looping=b;}
+
+ Sound* operator()() {
+ return new Sound (m_clock,
+ m_uri,
+ m_volume,
+ m_duration_minutes,
+ m_looping);
+ }
-void on_snap_closed(NotifyNotification*, gpointer)
-{
- stop_alarm_sound();
-}
+private:
+ std::shared_ptr<Clock> m_clock;
+ std::string m_uri;
+ unsigned int m_volume = 50;
+ unsigned int m_duration_minutes = 30;
+ bool m_looping = true;
+};
-void snap_data_destroy_notify(gpointer gdata)
+/**
+ * A popup notification (with optional sound)
+ * that emits a Response signal when done.
+ */
+class Popup
{
- delete static_cast<SnapData*>(gdata);
-}
+public:
-std::set<std::string> get_server_caps()
-{
- std::set<std::string> caps_set;
- auto caps_gl = notify_get_server_caps();
- std::string caps_str;
- for(auto l=caps_gl; l!=nullptr; l=l->next)
+ Popup(const Appointment& appointment, const SoundBuilder& sound_builder):
+ m_appointment(appointment),
+ m_interactive(get_interactive()),
+ m_sound_builder(sound_builder)
{
- caps_set.insert((const char*)l->data);
+ show();
+ }
- caps_str += (const char*) l->data;;
- if (l->next != nullptr)
- caps_str += ", ";
+ ~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);
+ }
}
- 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
- NOTIFY_MODE_BUBBLE,
+ typedef enum
+ {
+ RESPONSE_SHOW,
+ RESPONSE_DISMISS,
+ RESPONSE_CLOSE
+ }
+ Response;
- // a snap decision popup dialog + audio
- NOTIFY_MODE_SNAP
-}
-NotifyMode;
+ core::Signal<Response>& response() { return m_response; }
-NotifyMode get_notify_mode()
-{
- static NotifyMode mode;
- static bool mode_inited = false;
+private:
- if (G_UNLIKELY(!mode_inited))
+ void show()
{
- const auto caps = get_server_caps();
+ const Appointment& appointment = m_appointment;
- if (caps.count("actions"))
- mode = NOTIFY_MODE_SNAP;
- else
- mode = NOTIFY_MODE_BUBBLE;
+ /// 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)
+ {
+ const auto duration = std::chrono::minutes(m_sound_builder.duration_minutes());
+
+ notify_notification_set_hint(m_nn, HINT_SNAP,
+ g_variant_new_boolean(true));
+ notify_notification_set_hint(m_nn, HINT_TINT,
+ g_variant_new_boolean(true));
+ notify_notification_set_hint(m_nn, HINT_TIMEOUT,
+ g_variant_new_int32(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()));
+
+ /// 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);
+ }
- mode_inited = true;
+ 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);
}
- return mode;
-}
+ // user clicked 'show'
+ static void on_snap_show(NotifyNotification*, gchar*, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_response_value = RESPONSE_SHOW;
+ self->m_sound.reset();
+ }
-bool show_notification (SnapData* data, NotifyMode mode)
-{
- const Appointment& appointment = data->appointment;
+ // user clicked 'dismiss'
+ static void on_snap_dismiss(NotifyNotification*, gchar*, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_response_value = RESPONSE_DISMISS;
+ self->m_sound.reset();
+ }
- 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";
+ // the popup was closed
+ static void on_snap_closed(NotifyNotification*, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_sound.reset();
+ self->m_response(self->m_response_value);
+ }
- auto nn = notify_notification_new(title, body.c_str(), icon_name);
- if (mode == NOTIFY_MODE_SNAP)
+ /***
+ **** Interactive
+ ***/
+
+ static std::set<std::string> get_server_caps()
{
- notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true");
- notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true");
- /* text for the alarm popup dialog's button to show the active alarm */
- notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr);
- /* text for the alarm popup dialog's button to shut up the alarm */
- notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr);
- g_signal_connect(G_OBJECT(nn), "closed", G_CALLBACK(on_snap_closed), data);
+ std::set<std::string> 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;
}
- g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify);
- bool shown = true;
- GError * error = nullptr;
- notify_notification_show(nn, &error);
- if (error != NULL)
+ static bool get_interactive()
{
- g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
- g_error_free(error);
- data->show(data->appointment);
- shown = false;
+ static bool interactive;
+
+ static std::once_flag once;
+ std::call_once(once, [](){
+ interactive = get_server_caps().count("actions") != 0;
+ });
+
+ return interactive;
}
- g_free(title);
- return shown;
-}
+ /***
+ ****
+ ***/
+
+ typedef Popup Self;
+
+ const Appointment m_appointment;
+ const bool m_interactive;
+ SoundBuilder m_sound_builder;
+ std::unique_ptr<Sound> m_sound;
+ core::Signal<Response> m_response;
+ Response m_response_value = RESPONSE_CLOSE;
+ NotifyNotification* m_nn = nullptr;
+
+ static constexpr char const * HINT_SNAP {"x-canonical-snap-decisions"};
+ static constexpr char const * HINT_TINT {"x-canonical-private-button-tint"};
+ static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"};
+};
/**
-***
+*** libnotify -- snap decisions
**/
-void notify(const Appointment& appointment,
- Snap::appointment_func show,
- Snap::appointment_func dismiss)
+std::string get_alarm_uri(const Appointment& appointment,
+ const std::shared_ptr<const Settings>& settings)
{
- auto data = new SnapData;
- data->appointment = appointment;
- data->show = show;
- data->dismiss = dismiss;
+ const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
- switch (get_notify_mode())
+ const std::string candidates[] = { appointment.audio_url,
+ settings->alarm_sound.get(),
+ FALLBACK };
+
+ std::string uri;
+
+ for(const auto& candidate : candidates)
{
- case NOTIFY_MODE_BUBBLE:
- show_notification(data, NOTIFY_MODE_BUBBLE);
+ if (gst_uri_is_valid (candidate.c_str()))
+ {
+ uri = candidate;
break;
+ }
+ else if (g_file_test(candidate.c_str(), G_FILE_TEST_EXISTS))
+ {
+ gchar* tmp = gst_filename_to_uri(candidate.c_str(), nullptr);
+ if (tmp != nullptr)
+ {
+ uri = tmp;
+ g_free (tmp);
+ break;
+ }
+ }
+ }
- default:
- if (show_notification(data, NOTIFY_MODE_SNAP))
- play_alarm_sound();
- break;
- }
+ return uri;
}
-} // unnamed namespace
+int32_t n_existing_snaps = 0;
+} // unnamed namespace
/***
****
***/
-Snap::Snap()
+Snap::Snap(const std::shared_ptr<Clock>& clock,
+ const std::shared_ptr<const Settings>& settings):
+ m_clock(clock),
+ m_settings(settings)
{
- first_time_init();
+ if (!n_existing_snaps++ && !notify_init("indicator-datetime-service"))
+ g_critical("libnotify initialization failed");
}
Snap::~Snap()
{
- g_clear_pointer(&c_context, ca_context_destroy);
+ if (!--n_existing_snaps)
+ notify_uninit();
}
void Snap::operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss)
{
- if (appointment.has_alarms)
- notify(appointment, show, dismiss);
- else
+ if (!appointment.has_alarms)
+ {
dismiss(appointment);
+ return;
+ }
+
+ // create a popup...
+ SoundBuilder sound_builder;
+ sound_builder.set_uri(get_alarm_uri(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<Popup*>(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);
+ });
}
/***
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index fe6d7eb..20e744a 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -5,7 +5,12 @@ add_library (gtest STATIC
set_target_properties (gtest PROPERTIES INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} ${GTEST_INCLUDE_DIR})
set_target_properties (gtest PROPERTIES COMPILE_FLAGS ${COMPILE_FLAGS} -w)
-SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -g ${CC_WARNING_ARGS}")
+SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g ${CC_WARNING_ARGS}")
+
+# dbustest
+pkg_check_modules(DBUSTEST REQUIRED
+ dbustest-1>=14.04.0)
+include_directories (SYSTEM ${DBUSTEST_INCLUDE_DIRS})
# build the necessary schemas
set_directory_properties (PROPERTIES
@@ -21,15 +26,17 @@ execute_process (COMMAND ${PKG_CONFIG_EXECUTABLE} gio-2.0 --variable glib_compil
OUTPUT_VARIABLE COMPILE_SCHEMA_EXECUTABLE
OUTPUT_STRIP_TRAILING_WHITESPACE)
add_custom_command (OUTPUT gschemas.compiled
- DEPENDS ${CMAKE_SOURCE_DIR}/data/com.canonical.indicator.datetime.gschema.xml
- COMMAND cp -f ${CMAKE_SOURCE_DIR}/data/*gschema.xml ${SCHEMA_DIR}
+ DEPENDS ${CMAKE_BINARY_DIR}/data/com.canonical.indicator.datetime.gschema.xml
+ COMMAND cp -f ${CMAKE_BINARY_DIR}/data/*gschema.xml ${SCHEMA_DIR}
COMMAND ${COMPILE_SCHEMA_EXECUTABLE} ${SCHEMA_DIR})
# look for headers in our src dir, and also in the directories where we autogenerate files...
include_directories (${CMAKE_SOURCE_DIR}/src)
+include_directories (${CMAKE_BINARY_DIR}/src)
include_directories (${CMAKE_CURRENT_BINARY_DIR})
include_directories (${DBUSTEST_INCLUDE_DIRS})
+
add_definitions (-DSANDBOX="${CMAKE_CURRENT_BINARY_DIR}")
@@ -38,10 +45,12 @@ function(add_test_by_name name)
add_executable (${TEST_NAME} ${TEST_NAME}.cpp gschemas.compiled)
add_test (${TEST_NAME} ${TEST_NAME})
add_dependencies (${TEST_NAME} libindicatordatetimeservice)
- target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS})
+ target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${DBUSTEST_LIBRARIES} ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS})
endfunction()
+add_test_by_name(test-snap)
add_test_by_name(test-actions)
add_test_by_name(test-alarm-queue)
+add_test(NAME dear-reader-the-next-test-takes-60-seconds COMMAND true)
add_test_by_name(test-clock)
add_test_by_name(test-exporter)
add_test_by_name(test-formatter)
diff --git a/tests/manual b/tests/manual
index 17b4778..c5c52bc 100644
--- a/tests/manual
+++ b/tests/manual
@@ -22,3 +22,35 @@ Test-case indicator-datetime/unity8-items-check
<dd>The menu is populated with items</dd>
</dl>
+Test-case indicator-datetime/new-alarm-wakeup
+<dl>
+ <dt>Create and save an upcoming alarm in ubuntu-clock-app</dt>
+ <dt>Unplug the phone from any USB connection and put it to sleep</dt>
+ <dd>Confirm that the alarm sounds on time even if the phone is asleep.
+ (Note: if in doubt about sleep you can see in the syslog whether the
+ device actually suspended or whether the suspend was aborted)</dd>
+</dl>
+
+Test-case indicator-datetime/edited-alarm-wakeup
+<dl>
+ <dt>Edit an alarm that's already passed. (see previous test)</dt>
+ <dt>Unplug the phone from any USB connection and put it to sleep</dt>
+ <dd>Confirm that the alarm sounds on time even if the phone is asleep.
+ (Note: if in doubt about sleep you can see in the syslog whether the
+ device actually suspended or whether the suspend was aborted)</dd>
+</dl>
+
+Test-case indicator-datetime/tell-snap-decision-to-dismiss
+<dl>
+ <dt>Set an alarm and wait for it to arrive.</td>
+ <dd>Alarm should go off at the specified time</dd>
+ <dt>Press the 'Dismiss' button in the alarm's snap decision popup before the sound stops.</dt>
+ <dd>Popup should disappear</dd>
+ <dd>Sound should stop at the same time, rather than playing til the end of the file.</dd>
+</dl>
+
+
+<strong>
+ If all actions produce the expected results listed, please <a href="results#add_result">submit</a> a 'passed' result.
+ If an action fails, or produces an unexpected result, please <a href="results#add_result">submit</a> a 'failed' result and <a href="../../buginstructions">file a bug</a>. Please be sure to include the bug number when you <a href="results#add_result">submit</a> your result</strong>.
+
diff --git a/tests/manual-test-snap.cpp b/tests/manual-test-snap.cpp
index 16e606a..cc24a67 100644
--- a/tests/manual-test-snap.cpp
+++ b/tests/manual-test-snap.cpp
@@ -19,18 +19,51 @@
*/
#include <datetime/appointment.h>
+#include <datetime/settings-live.h>
#include <datetime/snap.h>
+#include <datetime/timezones-live.h>
#include <glib.h>
using namespace unity::indicator::datetime;
+#define TIMEZONE_FILE ("/etc/timezone")
+
+
/***
****
***/
-int main()
+namespace
+{
+ gboolean quit_idle (gpointer gloop)
+ {
+ g_main_loop_quit(static_cast<GMainLoop*>(gloop));
+ return G_SOURCE_REMOVE;
+ };
+
+ int volume = 50;
+
+ GOptionEntry entries[] =
+ {
+ { "volume", 'v', 0, G_OPTION_ARG_INT, &volume, "Volume level [1..100]", "volume" },
+ { NULL }
+ };
+}
+
+int main(int argc, const char* argv[])
{
+ GError* error = nullptr;
+ GOptionContext* context = g_option_context_new(nullptr);
+ g_option_context_add_main_entries(context, entries, nullptr);
+ if (!g_option_context_parse(context, &argc, (gchar***)&argv, &error))
+ {
+ g_print("option parsing failed: %s\n", error->message);
+ exit(1);
+ }
+ g_option_context_free(context);
+ volume = CLAMP(volume, 1, 100);
+
Appointment a;
a.color = "green";
a.summary = "Alarm";
@@ -47,15 +80,25 @@ int main()
auto loop = g_main_loop_new(nullptr, false);
auto show = [loop](const Appointment& appt){
g_message("You clicked 'show' for appt url '%s'", appt.url.c_str());
- g_main_loop_quit(loop);
+ g_idle_add(quit_idle, loop);
};
auto dismiss = [loop](const Appointment&){
g_message("You clicked 'dismiss'");
- g_main_loop_quit(loop);
+ g_idle_add(quit_idle, loop);
};
-
- Snap snap;
+
+ // only use local, temporary settings
+ g_assert(g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, true));
+ g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true));
+ g_debug("SCHEMA_DIR is %s", SCHEMA_DIR);
+
+ auto settings = std::make_shared<LiveSettings>();
+ settings->alarm_volume.set(volume);
+ auto timezones = std::make_shared<LiveTimezones>(settings, TIMEZONE_FILE);
+ auto clock = std::make_shared<LiveClock>(timezones);
+ Snap snap (clock, settings);
snap(a, show, dismiss);
g_main_loop_run(loop);
+ g_main_loop_unref(loop);
return 0;
}
diff --git a/tests/test-exporter.cpp b/tests/test-exporter.cpp
index a255ef9..e947740 100644
--- a/tests/test-exporter.cpp
+++ b/tests/test-exporter.cpp
@@ -21,6 +21,8 @@
#include "state-mock.h"
#include "glib-fixture.h"
+#include "dbus-alarm-properties.h"
+
#include <datetime/actions.h>
#include <datetime/dbus-shared.h>
#include <datetime/exporter.h>
@@ -76,13 +78,14 @@ TEST_F(ExporterFixture, Publish)
{
std::shared_ptr<State> state(new MockState);
std::shared_ptr<Actions> actions(new MockActions(state));
+ std::shared_ptr<Settings> settings(new Settings);
std::vector<std::shared_ptr<Menu>> menus;
MenuFactory menu_factory (actions, state);
for(int i=0; i<Menu::NUM_PROFILES; i++)
menus.push_back(menu_factory.buildMenu(Menu::Profile(i)));
- Exporter exporter;
+ Exporter exporter(settings);
exporter.publish(actions, menus);
wait_msec();
@@ -122,7 +125,7 @@ TEST_F(ExporterFixture, Publish)
// try closing the connection prematurely
// to test Exporter's name-lost signal
bool name_lost = false;
- exporter.name_lost.connect([this,&name_lost](){
+ exporter.name_lost().connect([this,&name_lost](){
name_lost = true;
g_main_loop_quit(loop);
});
@@ -135,3 +138,91 @@ TEST_F(ExporterFixture, Publish)
g_clear_object(&exported);
g_clear_object(&connection);
}
+
+TEST_F(ExporterFixture, AlarmProperties)
+{
+ /***
+ **** Set up the exporter
+ ***/
+
+ std::shared_ptr<State> state(new MockState);
+ std::shared_ptr<Actions> actions(new MockActions(state));
+ std::shared_ptr<Settings> settings(new Settings);
+ std::vector<std::shared_ptr<Menu>> menus;
+
+ MenuFactory menu_factory (actions, state);
+ for(int i=0; i<Menu::NUM_PROFILES; i++)
+ menus.push_back(menu_factory.buildMenu(Menu::Profile(i)));
+
+ Exporter exporter(settings);
+ exporter.publish(actions, menus);
+ wait_msec();
+
+ /***
+ **** Set up the proxy
+ ***/
+
+ auto on_proxy_ready = [](GObject*, GAsyncResult* res, gpointer gproxy){
+ GError* error = nullptr;
+ *reinterpret_cast<DatetimeAlarmProperties**>(gproxy) = datetime_alarm_properties_proxy_new_for_bus_finish(res, &error);
+ EXPECT_TRUE(error == nullptr);
+ };
+
+ DatetimeAlarmProperties* proxy = nullptr;
+ datetime_alarm_properties_proxy_new_for_bus(G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ BUS_NAME,
+ BUS_PATH"/AlarmProperties",
+ nullptr,
+ on_proxy_ready,
+ &proxy);
+ wait_msec(100);
+ ASSERT_TRUE(proxy != nullptr);
+
+ /***
+ **** Try changing the Settings -- do the DBus properties change to match it?
+ ***/
+
+ auto expected_volume = 1;
+ int expected_duration = 60;
+ const char * expected_sound = "/tmp/foo.wav";
+ settings->alarm_volume.set(expected_volume);
+ settings->alarm_duration.set(expected_duration);
+ settings->alarm_sound.set(expected_sound);
+ wait_msec();
+
+ static constexpr const char* const SOUND_PROP {"default-sound"};
+ static constexpr const char* const VOLUME_PROP {"default-volume"};
+ static constexpr const char* const DURATION_PROP {"duration"};
+
+ char* sound = nullptr;
+ int volume = -1;
+ int duration = -1;
+ g_object_get(proxy, SOUND_PROP, &sound,
+ VOLUME_PROP, &volume,
+ DURATION_PROP, &duration,
+ nullptr);
+ EXPECT_STREQ(expected_sound, sound);
+ EXPECT_EQ(expected_volume, volume);
+ EXPECT_EQ(expected_duration, duration);
+
+ /***
+ **** Try chaning the DBus properties -- do the Settings change to match it?
+ ***/
+
+ expected_volume = 100;
+ expected_duration = 30;
+ expected_sound = "/tmp/bar.wav";
+ g_object_set(proxy, SOUND_PROP, expected_sound,
+ VOLUME_PROP, expected_volume,
+ DURATION_PROP, expected_duration,
+ nullptr);
+ wait_msec();
+
+ EXPECT_STREQ(expected_sound, settings->alarm_sound.get().c_str());
+ EXPECT_EQ(expected_volume, settings->alarm_volume.get());
+ EXPECT_EQ(expected_duration, settings->alarm_duration.get());
+
+ // cleanup
+ g_clear_object(&proxy);
+}
diff --git a/tests/test-locations.cpp b/tests/test-locations.cpp
index 65adbc7..48e845a 100644
--- a/tests/test-locations.cpp
+++ b/tests/test-locations.cpp
@@ -139,7 +139,6 @@ TEST_F(LocationsFixture, ChangeLocationStrings)
EXPECT_EQ("Europe/London", l[3].zone());
EXPECT_EQ("Berlin", l[4].name());
EXPECT_EQ("Europe/Berlin", l[4].zone());
- locations_changed = false;
}
TEST_F(LocationsFixture, ChangeLocationVisibility)
diff --git a/tests/test-menus.cpp b/tests/test-menus.cpp
index b485037..f5f8df2 100644
--- a/tests/test-menus.cpp
+++ b/tests/test-menus.cpp
@@ -484,7 +484,7 @@ TEST_F(MenuFixture, HelloWorld)
EXPECT_EQ(Menu::NUM_PROFILES, m_menus.size());
for (int i=0; i<Menu::NUM_PROFILES; i++)
{
- EXPECT_TRUE(m_menus[i] != false);
+ EXPECT_TRUE(m_menus[i].get() != nullptr);
EXPECT_TRUE(m_menus[i]->menu_model() != nullptr);
EXPECT_EQ(i, m_menus[i]->profile());
}
diff --git a/tests/test-settings.cpp b/tests/test-settings.cpp
index 707247d..44a0252 100644
--- a/tests/test-settings.cpp
+++ b/tests/test-settings.cpp
@@ -100,6 +100,29 @@ protected:
EXPECT_EQ(str, tmp);
g_clear_pointer(&tmp, g_free);
}
+
+ void TestUIntProperty(core::Property<unsigned int>& property, const gchar* key)
+ {
+ EXPECT_EQ(g_settings_get_uint(m_gsettings, key), property.get());
+
+ unsigned int expected_values[] = { 1, 2, 3 };
+
+ // modify GSettings and confirm that the new value is propagated
+ for(const auto& expected_value : expected_values)
+ {
+ g_settings_set_uint(m_gsettings, key, expected_value);
+ EXPECT_EQ(expected_value, property.get());
+ EXPECT_EQ(expected_value, g_settings_get_uint(m_gsettings, key));
+ }
+
+ // modify the property and confirm that the new value is propagated
+ for(const auto& expected_value : expected_values)
+ {
+ property.set(expected_value);
+ EXPECT_EQ(expected_value, property.get());
+ EXPECT_EQ(expected_value, g_settings_get_uint(m_gsettings, key));
+ }
+ }
};
/***
@@ -125,10 +148,17 @@ TEST_F(SettingsFixture, BoolProperties)
TestBoolProperty(m_settings->show_year, SETTINGS_SHOW_YEAR_S);
}
+TEST_F(SettingsFixture, UIntProperties)
+{
+ TestUIntProperty(m_settings->alarm_duration, SETTINGS_ALARM_DURATION_S);
+ TestUIntProperty(m_settings->alarm_volume, SETTINGS_ALARM_VOLUME_S);
+}
+
TEST_F(SettingsFixture, StringProperties)
{
TestStringProperty(m_settings->custom_time_format, SETTINGS_CUSTOM_TIME_FORMAT_S);
TestStringProperty(m_settings->timezone_name, SETTINGS_TIMEZONE_NAME_S);
+ TestStringProperty(m_settings->alarm_sound, SETTINGS_ALARM_SOUND_S);
}
TEST_F(SettingsFixture, TimeFormatMode)
diff --git a/tests/test-snap.cpp b/tests/test-snap.cpp
new file mode 100644
index 0000000..efe30f5
--- /dev/null
+++ b/tests/test-snap.cpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <datetime/appointment.h>
+#include <datetime/settings.h>
+#include <datetime/snap.h>
+#include <datetime/timezones.h>
+
+#include <libdbustest/dbus-test.h>
+
+#include <libnotify/notify.h>
+
+#include <glib.h>
+
+using namespace unity::indicator::datetime;
+
+#include "glib-fixture.h"
+
+/***
+****
+***/
+
+using namespace unity::indicator::datetime;
+
+class SnapFixture: public GlibFixture
+{
+private:
+
+ typedef GlibFixture super;
+
+ static constexpr char const * NOTIFY_BUSNAME {"org.freedesktop.Notifications"};
+ static constexpr char const * NOTIFY_INTERFACE {"org.freedesktop.Notifications"};
+ static constexpr char const * NOTIFY_PATH {"/org/freedesktop/Notifications"};
+
+protected:
+
+ static constexpr int NOTIFY_ID {1234};
+
+ static constexpr int NOTIFICATION_CLOSED_EXPIRED {1};
+ static constexpr int NOTIFICATION_CLOSED_DISMISSED {2};
+ static constexpr int NOTIFICATION_CLOSED_API {3};
+ static constexpr int NOTIFICATION_CLOSED_UNDEFINED {4};
+
+ static constexpr char const * APP_NAME {"indicator-datetime-service"};
+
+ static constexpr char const * METHOD_NOTIFY {"Notify"};
+ static constexpr char const * METHOD_GET_CAPS {"GetCapabilities"};
+ static constexpr char const * METHOD_GET_INFO {"GetServerInformation"};
+
+ static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"};
+
+ DbusTestService * service = nullptr;
+ DbusTestDbusMock * mock = nullptr;
+ DbusTestDbusMockObject * obj = nullptr;
+ GDBusConnection * bus = nullptr;
+ Appointment appt;
+
+ void SetUp()
+ {
+ super::SetUp();
+
+ // init the Appointment
+ appt.color = "green";
+ appt.summary = "Alarm";
+ appt.url = "alarm:///hello-world";
+ appt.uid = "D4B57D50247291478ED31DED17FF0A9838DED402";
+ appt.has_alarms = true;
+ auto begin = g_date_time_new_local(2014,12,25,0,0,0);
+ auto end = g_date_time_add_full(begin,0,0,1,0,0,-1);
+ appt.begin = begin;
+ appt.end = end;
+ g_date_time_unref(end);
+ g_date_time_unref(begin);
+
+ //
+ // init DBusMock / dbus-test-runner
+ //
+
+ service = dbus_test_service_new(nullptr);
+
+ GError * error = nullptr;
+ mock = dbus_test_dbus_mock_new(NOTIFY_BUSNAME);
+ obj = dbus_test_dbus_mock_get_object(mock, NOTIFY_PATH, NOTIFY_INTERFACE, &error);
+ g_assert_no_error (error);
+
+ dbus_test_dbus_mock_object_add_method(mock, obj, METHOD_GET_INFO,
+ nullptr,
+ G_VARIANT_TYPE("(ssss)"),
+ "ret = ('mock-notify', 'test vendor', '1.0', '1.1')", // python
+ &error);
+ g_assert_no_error (error);
+
+ auto python_str = g_strdup_printf ("ret = %d", NOTIFY_ID);
+ dbus_test_dbus_mock_object_add_method(mock, obj, METHOD_NOTIFY,
+ G_VARIANT_TYPE("(susssasa{sv}i)"),
+ G_VARIANT_TYPE_UINT32,
+ python_str,
+ &error);
+ g_free (python_str);
+ g_assert_no_error (error);
+
+ dbus_test_service_add_task(service, DBUS_TEST_TASK(mock));
+ dbus_test_service_start_tasks(service);
+
+ bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
+ g_dbus_connection_set_exit_on_close(bus, FALSE);
+ g_object_add_weak_pointer(G_OBJECT(bus), (gpointer *)&bus);
+
+ notify_init(APP_NAME);
+ }
+
+ virtual void TearDown()
+ {
+ notify_uninit();
+
+ g_clear_object(&mock);
+ g_clear_object(&service);
+ g_object_unref(bus);
+
+ // wait a little while for the scaffolding to shut down,
+ // but don't block on it forever...
+ unsigned int cleartry = 0;
+ while ((bus != nullptr) && (cleartry < 50))
+ {
+ g_usleep(100000);
+ while (g_main_pending())
+ g_main_iteration(true);
+ cleartry++;
+ }
+
+ super::TearDown();
+ }
+};
+
+/***
+****
+***/
+
+namespace
+{
+ gboolean quit_idle (gpointer gloop)
+ {
+ g_main_loop_quit(static_cast<GMainLoop*>(gloop));
+ return G_SOURCE_REMOVE;
+ };
+}
+
+TEST_F(SnapFixture, InteractiveDuration)
+{
+ static constexpr int duration_minutes = 120;
+ auto settings = std::make_shared<Settings>();
+ settings->alarm_duration.set(duration_minutes);
+ auto timezones = std::make_shared<Timezones>();
+ auto clock = std::make_shared<LiveClock>(timezones);
+ Snap snap (clock, settings);
+
+ // GetCapabilities returns an array containing 'actions',
+ // so our snap decision will be interactive.
+ // For this test, it means we should get a timeout Notify Hint
+ // that matches duration_minutes
+ GError * error = nullptr;
+ dbus_test_dbus_mock_object_add_method(mock, obj, METHOD_GET_CAPS, nullptr, G_VARIANT_TYPE_STRING_ARRAY, "ret = ['actions', 'body']", &error);
+ g_assert_no_error (error);
+
+ // call the Snap Decision
+ auto func = [this](const Appointment&){g_idle_add(quit_idle, loop);};
+ snap(appt, func, func);
+
+ // confirm that Notify got called once
+ guint len = 0;
+ auto calls = dbus_test_dbus_mock_object_get_method_calls (mock, obj, METHOD_NOTIFY, &len, &error);
+ g_assert_no_error(error);
+ ASSERT_EQ(1, len);
+
+ // confirm that the app_name passed to Notify was APP_NAME
+ const auto& params = calls[0].params;
+ ASSERT_EQ(8, g_variant_n_children(params));
+ const char * str = nullptr;
+ g_variant_get_child (params, 0, "&s", &str);
+ ASSERT_STREQ(APP_NAME, str);
+
+ // confirm that the icon passed to Notify was "alarm-clock"
+ g_variant_get_child (params, 2, "&s", &str);
+ ASSERT_STREQ("alarm-clock", str);
+
+ // confirm that the hints passed to Notify included a timeout matching duration_minutes
+ int32_t i32;
+ bool b;
+ auto hints = g_variant_get_child_value (params, 6);
+ b = g_variant_lookup (hints, HINT_TIMEOUT, "i", &i32);
+ EXPECT_TRUE(b);
+ const auto duration = std::chrono::minutes(duration_minutes);
+ EXPECT_EQ(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count(), i32);
+ g_variant_unref(hints);
+}
+