Merge lp:~chipaca/ubuntu-push/the-push-automatic into lp:ubuntu-push

Proposed by John Lenton
Status: Merged
Approved by: John Lenton
Approved revision: no longer in the source branch.
Merged at revision: 107
Proposed branch: lp:~chipaca/ubuntu-push/the-push-automatic
Merge into: lp:ubuntu-push
Diff against target: 3302 lines (+1773/-357)
35 files modified
PACKAGE_DEPS (+1/-0)
bus/connectivity/connectivity.go (+1/-1)
bus/connectivity/connectivity_test.go (+1/-0)
bus/emblemcounter/emblemcounter.go (+69/-0)
bus/emblemcounter/emblemcounter_test.go (+79/-0)
bus/endpoint.go (+11/-0)
bus/haptic/haptic.go (+70/-0)
bus/haptic/haptic_test.go (+111/-0)
bus/notifications/app_helper/app_helper_c.go (+18/-13)
bus/testing/testing_endpoint.go (+15/-0)
bus/testing/testing_endpoint_test.go (+11/-0)
click/cclick/cclick.go (+76/-0)
click/click.go (+86/-0)
click/click_test.go (+86/-0)
client/client.go (+114/-31)
client/client_test.go (+227/-32)
client/service/common.go (+28/-6)
client/service/common_test.go (+60/-0)
client/service/postal.go (+55/-38)
client/service/postal_test.go (+111/-52)
client/service/service.go (+78/-78)
client/service/service_test.go (+181/-96)
client/session/session.go (+11/-0)
client/session/session_test.go (+61/-3)
debian/changelog (+15/-0)
debian/config.json (+1/-1)
debian/control (+1/-0)
launch_helper/helper_output.go (+2/-2)
messaging/messaging.go (+1/-0)
messaging/messaging_test.go (+4/-4)
server/session/session_test.go (+1/-0)
sounds/sounds.go (+91/-0)
sounds/sounds_test.go (+85/-0)
testing/helpers.go (+10/-0)
whoopsie/identifier/identifier.go (+1/-0)
To merge this branch: bzr merge lp:~chipaca/ubuntu-push/the-push-automatic
Reviewer Review Type Date Requested Status
John Lenton (community) Approve
Review via email: mp+225715@code.launchpad.net

Description of the change

Merging automatic:

[ Samuele Pedroni ]
  * Logic to support unregistering tokens lazily for uninstalled apps
  * Minimal wrapping of libclick to check if a package is installed for a user

[ John Lenton ]
  * Finalize dbus api
  * First version of emblem counters.

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) :
review: Approve
107. By PS Jenkins bot

* Logic to support unregistering tokens lazily for uninstalled apps
* Minimal wrapping of libclick to check if a package is installed for a user
* Refactor and cleanup of cleanup/service
* Finalized DBus API (hopefully)
* Support emblem counter notifications
* Support haptic (vibration) notifications
* Support sound notifications

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'PACKAGE_DEPS'
2--- PACKAGE_DEPS 2014-06-24 14:19:31 +0000
3+++ PACKAGE_DEPS 2014-07-07 14:23:18 +0000
4@@ -10,3 +10,4 @@
5 libwhoopsie-dev
6 libmessaging-menu-dev
7 libubuntu-app-launch2-dev
8+libclick-0.4-dev
9
10=== modified file 'bus/connectivity/connectivity.go'
11--- bus/connectivity/connectivity.go 2014-04-28 01:56:21 +0000
12+++ bus/connectivity/connectivity.go 2014-07-07 14:23:18 +0000
13@@ -129,7 +129,7 @@
14 case v, ok := <-cs.networkStateCh:
15 if !ok {
16 // tear it all down and start over
17- return false, errors.New("Got not-OK from StateChanged watch")
18+ return false, errors.New("got not-OK from StateChanged watch")
19 }
20 cs.webgetCh = nil
21 cs.currentState = v
22
23=== modified file 'bus/connectivity/connectivity_test.go'
24--- bus/connectivity/connectivity_test.go 2014-07-01 11:55:30 +0000
25+++ bus/connectivity/connectivity_test.go 2014-07-07 14:23:18 +0000
26@@ -167,6 +167,7 @@
27 func (*racyEndpoint) GrabName(bool) <-chan error { return nil }
28 func (*racyEndpoint) WatchMethod(bus.DispatchMap, string, ...interface{}) {}
29 func (*racyEndpoint) Signal(member string, suffix string, args []interface{}) error { return nil }
30+func (*racyEndpoint) SetProperty(string, string, interface{}) error { return nil }
31
32 var _ bus.Endpoint = (*racyEndpoint)(nil)
33
34
35=== added directory 'bus/emblemcounter'
36=== added file 'bus/emblemcounter/emblemcounter.go'
37--- bus/emblemcounter/emblemcounter.go 1970-01-01 00:00:00 +0000
38+++ bus/emblemcounter/emblemcounter.go 2014-07-07 14:23:18 +0000
39@@ -0,0 +1,69 @@
40+/*
41+ Copyright 2014 Canonical Ltd.
42+
43+ This program is free software: you can redistribute it and/or modify it
44+ under the terms of the GNU General Public License version 3, as published
45+ by the Free Software Foundation.
46+
47+ This program is distributed in the hope that it will be useful, but
48+ WITHOUT ANY WARRANTY; without even the implied warranties of
49+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
50+ PURPOSE. See the GNU General Public License for more details.
51+
52+ You should have received a copy of the GNU General Public License along
53+ with this program. If not, see <http://www.gnu.org/licenses/>.
54+*/
55+
56+// Package emblemcounter can present notifications as a counter on an
57+// emblem on an item in the launcher.
58+package emblemcounter
59+
60+import (
61+ "launchpad.net/go-dbus/v1"
62+
63+ "launchpad.net/ubuntu-push/bus"
64+ "launchpad.net/ubuntu-push/click"
65+ "launchpad.net/ubuntu-push/launch_helper"
66+ "launchpad.net/ubuntu-push/logger"
67+ "launchpad.net/ubuntu-push/nih"
68+)
69+
70+// emblemcounter works by setting properties on a well-known dbus name.
71+var BusAddress = bus.Address{
72+ Interface: "com.canonical.Unity.Launcher.Item",
73+ Path: "/com/canonical/Unity/Launcher",
74+ Name: "com.canonical.Unity.Launcher",
75+}
76+
77+// EmblemCounter is a little tool that fiddles with the unity launcher
78+// to put emblems with counters on launcher icons.
79+type EmblemCounter struct {
80+ bus bus.Endpoint
81+ log logger.Logger
82+}
83+
84+// Build an EmblemCounter using the given bus and log.
85+func New(endp bus.Endpoint, log logger.Logger) *EmblemCounter {
86+ return &EmblemCounter{bus: endp, log: log}
87+}
88+
89+// Look for an EmblemCounter section in a Notification and, if
90+// present, presents it to the user.
91+func (ctr *EmblemCounter) Present(appId string, notificationId string, notification *launch_helper.Notification) {
92+ if notification == nil || notification.EmblemCounter == nil {
93+ ctr.log.Debugf("no notification or no EmblemCounter in the notification; doing nothing: %#v", notification)
94+ return
95+ }
96+ parsed, err := click.ParseAppId(appId)
97+ if err != nil {
98+ ctr.log.Debugf("no appId in %#v", appId)
99+ return
100+ }
101+ ec := notification.EmblemCounter
102+ ctr.log.Debugf("setting emblem counter for %s to %d (visible: %t)", appId, ec.Count, ec.Visible)
103+
104+ quoted := string(nih.Quote([]byte(parsed.Application)))
105+
106+ ctr.bus.SetProperty("count", "/"+quoted, dbus.Variant{ec.Count})
107+ ctr.bus.SetProperty("countVisible", "/"+quoted, dbus.Variant{ec.Visible})
108+}
109
110=== added file 'bus/emblemcounter/emblemcounter_test.go'
111--- bus/emblemcounter/emblemcounter_test.go 1970-01-01 00:00:00 +0000
112+++ bus/emblemcounter/emblemcounter_test.go 2014-07-07 14:23:18 +0000
113@@ -0,0 +1,79 @@
114+/*
115+ Copyright 2014 Canonical Ltd.
116+
117+ This program is free software: you can redistribute it and/or modify it
118+ under the terms of the GNU General Public License version 3, as published
119+ by the Free Software Foundation.
120+
121+ This program is distributed in the hope that it will be useful, but
122+ WITHOUT ANY WARRANTY; without even the implied warranties of
123+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
124+ PURPOSE. See the GNU General Public License for more details.
125+
126+ You should have received a copy of the GNU General Public License along
127+ with this program. If not, see <http://www.gnu.org/licenses/>.
128+*/
129+
130+package emblemcounter
131+
132+import (
133+ "testing"
134+
135+ "launchpad.net/go-dbus/v1"
136+ . "launchpad.net/gocheck"
137+
138+ testibus "launchpad.net/ubuntu-push/bus/testing"
139+ "launchpad.net/ubuntu-push/launch_helper"
140+ helpers "launchpad.net/ubuntu-push/testing"
141+ "launchpad.net/ubuntu-push/testing/condition"
142+)
143+
144+func TestEmblemCounter(t *testing.T) { TestingT(t) }
145+
146+type ecSuite struct {
147+ log *helpers.TestLogger
148+}
149+
150+var _ = Suite(&ecSuite{})
151+
152+func (ecs *ecSuite) SetUpTest(c *C) {
153+ ecs.log = helpers.NewTestLogger(c, "debug")
154+}
155+
156+// checks that Present() actually calls SetProperty on the launcher
157+func (ecs *ecSuite) TestPresentPresents(c *C) {
158+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
159+
160+ ec := New(endp, ecs.log)
161+ notif := launch_helper.Notification{EmblemCounter: &launch_helper.EmblemCounter{Count: 42, Visible: true}}
162+ ec.Present("com.example.test_test-app", "nid", &notif)
163+ callArgs := testibus.GetCallArgs(endp)
164+ c.Assert(callArgs, HasLen, 2)
165+ c.Check(callArgs[0].Member, Equals, "::SetProperty")
166+ c.Check(callArgs[1].Member, Equals, "::SetProperty")
167+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", "/test_2dapp", dbus.Variant{Value: int32(42)}})
168+ c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", "/test_2dapp", dbus.Variant{Value: true}})
169+}
170+
171+// check that Present() doesn't call SetProperty if no EmblemCounter in the Notification
172+func (ecs *ecSuite) TestSkipIfMissing(c *C) {
173+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
174+ ec := New(endp, ecs.log)
175+
176+ // nothing happens if nil Notification
177+ ec.Present("com.example.test_test-app", "nid", nil)
178+ c.Assert(testibus.GetCallArgs(endp), HasLen, 0)
179+
180+ // nothing happens if no EmblemCounter in Notification
181+ ec.Present("com.example.test_test-app", "nid", &launch_helper.Notification{})
182+ c.Assert(testibus.GetCallArgs(endp), HasLen, 0)
183+
184+ // but an empty EmblemCounter is acted on
185+ ec.Present("com.example.test_test-app", "nid", &launch_helper.Notification{EmblemCounter: &launch_helper.EmblemCounter{}})
186+ callArgs := testibus.GetCallArgs(endp)
187+ c.Assert(callArgs, HasLen, 2)
188+ c.Check(callArgs[0].Member, Equals, "::SetProperty")
189+ c.Check(callArgs[1].Member, Equals, "::SetProperty")
190+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", "/test_2dapp", dbus.Variant{Value: int32(0)}})
191+ c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", "/test_2dapp", dbus.Variant{Value: false}})
192+}
193
194=== modified file 'bus/endpoint.go'
195--- bus/endpoint.go 2014-07-01 11:55:30 +0000
196+++ bus/endpoint.go 2014-07-07 14:23:18 +0000
197@@ -42,6 +42,7 @@
198 Signal(string, string, []interface{}) error
199 Call(member string, args []interface{}, rvs ...interface{}) error
200 GetProperty(property string) (interface{}, error)
201+ SetProperty(property string, suffix string, value interface{}) error
202 Dial() error
203 Close()
204 String() string
205@@ -178,6 +179,16 @@
206 return variant.Value, nil
207 }
208
209+// SetProperty calls org.freedesktop.DBus.Properties's Set method
210+//
211+// XXX: untested
212+func (endp *endpoint) SetProperty(property string, suffix string, value interface{}) error {
213+ // can't use the pre-existing ObjectProxy for this one
214+ proxy := endp.bus.Object(endp.addr.Name, dbus.ObjectPath(endp.addr.Path+suffix))
215+ _, err := proxy.Call("org.freedesktop.DBus.Properties", "Set", endp.addr.Interface, property, value)
216+ return err
217+}
218+
219 // Close the connection to dbus.
220 //
221 // XXX: untested
222
223=== added directory 'bus/haptic'
224=== added file 'bus/haptic/haptic.go'
225--- bus/haptic/haptic.go 1970-01-01 00:00:00 +0000
226+++ bus/haptic/haptic.go 2014-07-07 14:23:18 +0000
227@@ -0,0 +1,70 @@
228+/*
229+ Copyright 2014 Canonical Ltd.
230+
231+ This program is free software: you can redistribute it and/or modify it
232+ under the terms of the GNU General Public License version 3, as published
233+ by the Free Software Foundation.
234+
235+ This program is distributed in the hope that it will be useful, but
236+ WITHOUT ANY WARRANTY; without even the implied warranties of
237+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
238+ PURPOSE. See the GNU General Public License for more details.
239+
240+ You should have received a copy of the GNU General Public License along
241+ with this program. If not, see <http://www.gnu.org/licenses/>.
242+*/
243+
244+// Package haptic can present notifications as a vibration pattern
245+// using the usensord/haptic interface
246+package haptic
247+
248+import (
249+ "launchpad.net/ubuntu-push/bus"
250+ "launchpad.net/ubuntu-push/launch_helper"
251+ "launchpad.net/ubuntu-push/logger"
252+)
253+
254+// usensord/haptic lives on a well-knwon bus.Address
255+var BusAddress bus.Address = bus.Address{
256+ Interface: "com.canonical.usensord.haptic",
257+ Path: "/com/canonical/usensord/haptic",
258+ Name: "com.canonical.usensord",
259+}
260+
261+// Haptic encapsulates info needed to call out to usensord/haptic
262+type Haptic struct {
263+ bus bus.Endpoint
264+ log logger.Logger
265+}
266+
267+// New returns a new Haptic that'll use the provided bus.Endpoint
268+func New(endp bus.Endpoint, log logger.Logger) *Haptic {
269+ return &Haptic{endp, log}
270+}
271+
272+// Present presents the notification via a vibrate pattern
273+func (haptic *Haptic) Present(_, _ string, notification *launch_helper.Notification) bool {
274+ if notification == nil || notification.Vibrate == nil {
275+ haptic.log.Debugf("no notification or no Vibrate in the notification; doing nothing: %#v", notification)
276+ return false
277+ }
278+ pattern := notification.Vibrate.Pattern
279+ repeat := notification.Vibrate.Repeat
280+ if repeat == 0 {
281+ repeat = 1
282+ }
283+ if notification.Vibrate.Duration != 0 {
284+ pattern = []uint32{notification.Vibrate.Duration}
285+ }
286+ if len(pattern) == 0 {
287+ haptic.log.Debugf("not enough information in the notification's Vibrate section to create a pattern")
288+ return false
289+ }
290+ haptic.log.Debugf("vibrating %d times to the tune of %v", repeat, pattern)
291+ err := haptic.bus.Call("VibratePattern", bus.Args(pattern, repeat))
292+ if err != nil {
293+ haptic.log.Debugf("VibratePattern call returned %v", err)
294+ return false
295+ }
296+ return true
297+}
298
299=== added file 'bus/haptic/haptic_test.go'
300--- bus/haptic/haptic_test.go 1970-01-01 00:00:00 +0000
301+++ bus/haptic/haptic_test.go 2014-07-07 14:23:18 +0000
302@@ -0,0 +1,111 @@
303+/*
304+ Copyright 2014 Canonical Ltd.
305+
306+ This program is free software: you can redistribute it and/or modify it
307+ under the terms of the GNU General Public License version 3, as published
308+ by the Free Software Foundation.
309+
310+ This program is distributed in the hope that it will be useful, but
311+ WITHOUT ANY WARRANTY; without even the implied warranties of
312+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
313+ PURPOSE. See the GNU General Public License for more details.
314+
315+ You should have received a copy of the GNU General Public License along
316+ with this program. If not, see <http://www.gnu.org/licenses/>.
317+*/
318+
319+package haptic
320+
321+import (
322+ "testing"
323+
324+ . "launchpad.net/gocheck"
325+
326+ testibus "launchpad.net/ubuntu-push/bus/testing"
327+ "launchpad.net/ubuntu-push/launch_helper"
328+ helpers "launchpad.net/ubuntu-push/testing"
329+ "launchpad.net/ubuntu-push/testing/condition"
330+)
331+
332+func TestHaptic(t *testing.T) { TestingT(t) }
333+
334+type hapticSuite struct {
335+ log *helpers.TestLogger
336+}
337+
338+var _ = Suite(&hapticSuite{})
339+
340+func (hs *hapticSuite) SetUpTest(c *C) {
341+ hs.log = helpers.NewTestLogger(c, "debug")
342+}
343+
344+// checks that Present() actually calls VibratePattern
345+func (hs *hapticSuite) TestPresentPresents(c *C) {
346+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
347+
348+ ec := New(endp, hs.log)
349+ notif := launch_helper.Notification{Vibrate: &launch_helper.Vibration{Pattern: []uint32{200, 100}, Repeat: 2}}
350+ c.Check(ec.Present("com.example.test_test-app", "nid", &notif), Equals, true)
351+ callArgs := testibus.GetCallArgs(endp)
352+ c.Assert(callArgs, HasLen, 1)
353+ c.Check(callArgs[0].Member, Equals, "VibratePattern")
354+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{[]uint32{200, 100}, uint32(2)})
355+}
356+
357+// check that Present() defaults Repeat to 1
358+func (hs *hapticSuite) TestPresentDefaultsRepeatTo1(c *C) {
359+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
360+
361+ ec := New(endp, hs.log)
362+ // note: no Repeat:
363+ notif := launch_helper.Notification{Vibrate: &launch_helper.Vibration{Pattern: []uint32{200, 100}}}
364+ c.Check(ec.Present("com.example.test_test-app", "nid", &notif), Equals, true)
365+ callArgs := testibus.GetCallArgs(endp)
366+ c.Assert(callArgs, HasLen, 1)
367+ c.Check(callArgs[0].Member, Equals, "VibratePattern")
368+ // note: Repeat of 1:
369+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{[]uint32{200, 100}, uint32(1)})
370+}
371+
372+// check that Present() makes a Pattern of [Duration] if Duration is given
373+func (hs *hapticSuite) TestPresentBuildsPatternWithDuration(c *C) {
374+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
375+
376+ ec := New(endp, hs.log)
377+ // note: no Repeat, no Pattern, just Duration:
378+ notif := launch_helper.Notification{Vibrate: &launch_helper.Vibration{Duration: 200}}
379+ c.Check(ec.Present("com.example.test_test-app", "nid", &notif), Equals, true)
380+ callArgs := testibus.GetCallArgs(endp)
381+ c.Assert(callArgs, HasLen, 1)
382+ c.Check(callArgs[0].Member, Equals, "VibratePattern")
383+ // note: Pattern of [Duration], Repeat of 1:
384+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{[]uint32{200}, uint32(1)})
385+}
386+
387+// check that Present() ignores Pattern and makes a Pattern of [Duration] if Duration is given
388+func (hs *hapticSuite) TestPresentOverrides(c *C) {
389+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
390+
391+ ec := New(endp, hs.log)
392+ // note: Duration given, as well as Pattern; Repeat given as 0:
393+ notif := launch_helper.Notification{Vibrate: &launch_helper.Vibration{Duration: 200, Pattern: []uint32{500}, Repeat: 0}}
394+ c.Check(ec.Present("com.example.test_test-app", "nid", &notif), Equals, true)
395+ callArgs := testibus.GetCallArgs(endp)
396+ c.Assert(callArgs, HasLen, 1)
397+ c.Check(callArgs[0].Member, Equals, "VibratePattern")
398+ // note: Pattern of [Duration], Repeat of 1:
399+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{[]uint32{200}, uint32(1)})
400+}
401+
402+// check that Present() doesn't call VibratePattern if things are not right
403+func (hs *hapticSuite) TestSkipIfMissing(c *C) {
404+ endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
405+
406+ ec := New(endp, hs.log)
407+ // no notification at all
408+ c.Check(ec.Present("", "", nil), Equals, false)
409+ // no Vibration in the notificaton
410+ c.Check(ec.Present("", "", &launch_helper.Notification{}), Equals, false)
411+ // empty Vibration
412+ c.Check(ec.Present("", "", &launch_helper.Notification{Vibrate: &launch_helper.Vibration{}}), Equals, false)
413+}
414
415=== modified file 'bus/notifications/app_helper/app_helper_c.go'
416--- bus/notifications/app_helper/app_helper_c.go 2014-06-26 17:03:34 +0000
417+++ bus/notifications/app_helper/app_helper_c.go 2014-07-07 14:23:18 +0000
418@@ -19,23 +19,28 @@
419
420 /*
421 #cgo pkg-config: gio-unix-2.0
422-#cgo pkg-config: gio-2.0
423-#include <stdlib.h>
424 #include <glib.h>
425 #include <gio/gdesktopappinfo.h>
426+
427+gchar* app_icon_filename_from_id (gchar* app_id) {
428+ gchar* filename = NULL;
429+ GAppInfo* app_info = (GAppInfo*)g_desktop_app_info_new (app_id);
430+ if (app_info != NULL) {
431+ GIcon* icon = g_app_info_get_icon (app_info);
432+ if (icon != NULL) {
433+ filename = g_icon_to_string (icon);
434+ // g_app_info_get_icon has "transfer none"
435+ }
436+ g_object_unref (app_info);
437+ }
438+ g_free (app_id);
439+ return filename;
440+}
441 */
442 import "C"
443-import "unsafe"
444
445 func AppIconFromId(appId string) string {
446- _id := C.CString(appId)
447- defer C.free(unsafe.Pointer(_id))
448- _app_info := C.g_desktop_app_info_new(_id)
449- defer C.g_app_info_delete(_app_info)
450- _app_icon := C.g_app_info_get_icon(_app_info)
451- defer C.g_object_unref((C.gpointer)(_app_icon))
452- _icon_string := C.g_icon_to_string(_app_icon)
453- defer C.free(unsafe.Pointer(_icon_string))
454- name := C.GoString((*C.char)(_icon_string))
455- return name
456+ name := C.app_icon_filename_from_id((*C.gchar)(C.CString(appId)))
457+ defer C.g_free((C.gpointer)(name))
458+ return C.GoString((*C.char)(name))
459 }
460
461=== modified file 'bus/testing/testing_endpoint.go'
462--- bus/testing/testing_endpoint.go 2014-07-01 11:55:30 +0000
463+++ bus/testing/testing_endpoint.go 2014-07-07 14:23:18 +0000
464@@ -150,6 +150,21 @@
465 return res, err
466 }
467
468+// See Endpoint's SetProperty. This one does nothing beyond
469+// registering being called.
470+func (tc *testingEndpoint) SetProperty(property string, suffix string, value interface{}) error {
471+ tc.callArgsLck.Lock()
472+ defer tc.callArgsLck.Unlock()
473+
474+ args := callArgs{
475+ Member: "::SetProperty",
476+ Args: []interface{}{property, suffix, value},
477+ }
478+ tc.callArgs = append(tc.callArgs, args)
479+
480+ return nil
481+}
482+
483 // See Endpoint's Dial. This one will check its dialCondition to
484 // decide whether to return an error or not.
485 func (endp *testingEndpoint) Dial() error {
486
487=== modified file 'bus/testing/testing_endpoint_test.go'
488--- bus/testing/testing_endpoint_test.go 2014-07-01 11:55:30 +0000
489+++ bus/testing/testing_endpoint_test.go 2014-07-07 14:23:18 +0000
490@@ -238,3 +238,14 @@
491 Args: []interface{}{foomp, []interface{}(nil)},
492 }})
493 }
494+
495+// Test that SetProperty updates callArgs
496+func (s *TestingEndpointSuite) TestSetPropertyUpdatesCallArgs(c *C) {
497+ endp := NewTestingEndpoint(nil, condition.Work(true))
498+ endp.SetProperty("prop", "suffix", "value")
499+ c.Check(GetCallArgs(endp), DeepEquals, []callArgs{
500+ {
501+ Member: "::SetProperty",
502+ Args: []interface{}{"prop", "suffix", "value"},
503+ }})
504+}
505
506=== added directory 'click'
507=== added directory 'click/cclick'
508=== added file 'click/cclick/cclick.go'
509--- click/cclick/cclick.go 1970-01-01 00:00:00 +0000
510+++ click/cclick/cclick.go 2014-07-07 14:23:18 +0000
511@@ -0,0 +1,76 @@
512+/*
513+ Copyright 2013-2014 Canonical Ltd.
514+
515+ This program is free software: you can redistribute it and/or modify it
516+ under the terms of the GNU General Public License version 3, as published
517+ by the Free Software Foundation.
518+
519+ This program is distributed in the hope that it will be useful, but
520+ WITHOUT ANY WARRANTY; without even the implied warranties of
521+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
522+ PURPOSE. See the GNU General Public License for more details.
523+
524+ You should have received a copy of the GNU General Public License along
525+ with this program. If not, see <http://www.gnu.org/licenses/>.
526+*/
527+
528+// Package cclick has the internal cgo wrapping libclick for package click.
529+package cclick
530+
531+/*
532+#cgo pkg-config: click-0.4
533+#cgo pkg-config: glib-2.0 gobject-2.0
534+
535+#include <click-0.4/click.h>
536+*/
537+import "C"
538+
539+import (
540+ "fmt"
541+ "runtime"
542+)
543+
544+type CClickUser struct {
545+ cref *C.ClickUser
546+}
547+
548+func gchar(s string) *C.gchar {
549+ return (*C.gchar)(C.CString(s))
550+}
551+
552+func gfree(s *C.gchar) {
553+ C.g_free((C.gpointer)(s))
554+}
555+
556+func (ccu *CClickUser) CInit(holder interface{}) error {
557+ var gerr *C.GError
558+ cref := C.click_user_new_for_user(nil, nil, &gerr)
559+ defer C.g_clear_error(&gerr)
560+ if gerr != nil {
561+ return fmt.Errorf("faild to make ClickUser: %s", C.GoString((*C.char)(gerr.message)))
562+ }
563+ ccu.cref = cref
564+ runtime.SetFinalizer(holder, func(interface{}) {
565+ C.g_object_unref((C.gpointer)(cref))
566+ })
567+ return nil
568+}
569+
570+func (ccu *CClickUser) CGetVersion(pkgName string) string {
571+ pkgname := gchar(pkgName)
572+ defer gfree(pkgname)
573+ var gerr *C.GError
574+ defer C.g_clear_error(&gerr)
575+ ver := C.click_user_get_version(ccu.cref, pkgname, &gerr)
576+ if gerr != nil {
577+ return ""
578+ }
579+ defer gfree(ver)
580+ return C.GoString((*C.char)(ver))
581+}
582+
583+func (ccu *CClickUser) CHasPackageName(pkgName string) bool {
584+ pkgname := gchar(pkgName)
585+ defer gfree(pkgname)
586+ return C.click_user_has_package_name(ccu.cref, pkgname) == C.TRUE
587+}
588
589=== added file 'click/click.go'
590--- click/click.go 1970-01-01 00:00:00 +0000
591+++ click/click.go 2014-07-07 14:23:18 +0000
592@@ -0,0 +1,86 @@
593+/*
594+ Copyright 2014 Canonical Ltd.
595+
596+ This program is free software: you can redistribute it and/or modify it
597+ under the terms of the GNU General Public License version 3, as published
598+ by the Free Software Foundation.
599+
600+ This program is distributed in the hope that it will be useful, but
601+ WITHOUT ANY WARRANTY; without even the implied warranties of
602+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
603+ PURPOSE. See the GNU General Public License for more details.
604+
605+ You should have received a copy of the GNU General Public License along
606+ with this program. If not, see <http://www.gnu.org/licenses/>.
607+*/
608+
609+// Package click exposes some utilities related to click packages and
610+// wraps libclick to check if packages are installed.
611+package click
612+
613+import (
614+ "errors"
615+ "regexp"
616+ "sync"
617+
618+ "launchpad.net/ubuntu-push/click/cclick"
619+)
620+
621+// AppId holds a parsed application id.
622+type AppId struct {
623+ Package string
624+ Application string
625+ Version string
626+}
627+
628+// from https://wiki.ubuntu.com/AppStore/Interfaces/ApplicationId
629+// except the version is made optional
630+var rx = regexp.MustCompile(`^([a-z0-9][a-z0-9+.-]+)_([a-zA-Z0-9+.-]+)(?:_([0-9][a-zA-Z0-9.+:~-]*))?$`)
631+
632+var (
633+ ErrInvalidAppId = errors.New("invalid application id")
634+)
635+
636+func ParseAppId(id string) (*AppId, error) {
637+ m := rx.FindStringSubmatch(id)
638+ if len(m) == 0 {
639+ return nil, ErrInvalidAppId
640+ }
641+ return &AppId{Package: m[1], Application: m[2], Version: m[3]}, nil
642+}
643+
644+func AppInPackage(appId, pkgname string) bool {
645+ id, _ := ParseAppId(appId)
646+ return id != nil && id.Package == pkgname
647+}
648+
649+// ClickUser exposes the click package registry for the user.
650+type ClickUser struct {
651+ ccu cclick.CClickUser
652+ lock sync.Mutex
653+}
654+
655+// User makes a new ClickUser object for the current user.
656+func User() (*ClickUser, error) {
657+ cu := new(ClickUser)
658+ err := cu.ccu.CInit(cu)
659+ if err != nil {
660+ return nil, err
661+ }
662+ return cu, nil
663+}
664+
665+// HasPackage checks if the appId is installed for user.
666+func (cu *ClickUser) HasPackage(appId string) bool {
667+ cu.lock.Lock()
668+ defer cu.lock.Unlock()
669+ id, err := ParseAppId(appId)
670+ if err != nil {
671+ return false
672+ }
673+ if id.Version != "" {
674+ return cu.ccu.CGetVersion(id.Package) == id.Version
675+ } else {
676+ return cu.ccu.CHasPackageName(id.Package)
677+ }
678+}
679
680=== added file 'click/click_test.go'
681--- click/click_test.go 1970-01-01 00:00:00 +0000
682+++ click/click_test.go 2014-07-07 14:23:18 +0000
683@@ -0,0 +1,86 @@
684+/*
685+ Copyright 2013-2014 Canonical Ltd.
686+
687+ This program is free software: you can redistribute it and/or modify it
688+ under the terms of the GNU General Public License version 3, as published
689+ by the Free Software Foundation.
690+
691+ This program is distributed in the hope that it will be useful, but
692+ WITHOUT ANY WARRANTY; without even the implied warranties of
693+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
694+ PURPOSE. See the GNU General Public License for more details.
695+
696+ You should have received a copy of the GNU General Public License along
697+ with this program. If not, see <http://www.gnu.org/licenses/>.
698+*/
699+
700+package click
701+
702+import (
703+ "testing"
704+
705+ . "launchpad.net/gocheck"
706+)
707+
708+func TestClick(t *testing.T) { TestingT(t) }
709+
710+type clickSuite struct{}
711+
712+var _ = Suite(&clickSuite{})
713+
714+func (cs *clickSuite) TestParseAppId(c *C) {
715+ id, err := ParseAppId("com.ubuntu.clock_clock")
716+ c.Assert(err, IsNil)
717+ c.Check(id.Package, Equals, "com.ubuntu.clock")
718+ c.Check(id.Application, Equals, "clock")
719+ c.Check(id.Version, Equals, "")
720+
721+ id, err = ParseAppId("com.ubuntu.clock_clock_10")
722+ c.Assert(err, IsNil)
723+ c.Check(id.Package, Equals, "com.ubuntu.clock")
724+ c.Check(id.Application, Equals, "clock")
725+ c.Check(id.Version, Equals, "10")
726+
727+ for _, s := range []string{"com.ubuntu.clock_clock_10_4", "com.ubuntu.clock", ""} {
728+ id, err = ParseAppId(s)
729+ c.Check(id, IsNil)
730+ c.Check(err, Equals, ErrInvalidAppId)
731+ }
732+}
733+
734+func (cs *clickSuite) TestInPackage(c *C) {
735+ c.Check(AppInPackage("com.ubuntu.clock_clock", "com.ubuntu.clock"), Equals, true)
736+ c.Check(AppInPackage("com.ubuntu.clock_clock_10", "com.ubuntu.clock"), Equals, true)
737+ c.Check(AppInPackage("com.ubuntu.clock", "com.ubuntu.clock"), Equals, false)
738+ c.Check(AppInPackage("bananas", "fruit"), Equals, false)
739+}
740+
741+func (s *clickSuite) TestUser(c *C) {
742+ u, err := User()
743+ c.Assert(err, IsNil)
744+ c.Assert(u, NotNil)
745+}
746+
747+func (s *clickSuite) TestHasPackageNegative(c *C) {
748+ u, err := User()
749+ c.Assert(err, IsNil)
750+ c.Check(u.HasPackage("com.foo.bar"), Equals, false)
751+ c.Check(u.HasPackage("com.foo.bar_baz"), Equals, false)
752+}
753+
754+func (s *clickSuite) TestHasPackageVersionNegative(c *C) {
755+ u, err := User()
756+ c.Assert(err, IsNil)
757+ c.Check(u.HasPackage("com.ubuntu.clock_clock_1000.0"), Equals, false)
758+}
759+
760+func (s *clickSuite) TestHasPackageClock(c *C) {
761+ u, err := User()
762+ c.Assert(err, IsNil)
763+ ver := u.ccu.CGetVersion("com.ubuntu.clock")
764+ if ver == "" {
765+ c.Skip("no com.ubuntu.clock pkg installed")
766+ }
767+ c.Check(u.HasPackage("com.ubuntu.clock_clock"), Equals, true)
768+ c.Check(u.HasPackage("com.ubuntu.clock_clock_"+ver), Equals, true)
769+}
770
771=== modified file 'client/client.go'
772--- client/client.go 2014-07-01 11:55:30 +0000
773+++ client/client.go 2014-07-07 14:23:18 +0000
774@@ -26,16 +26,20 @@
775 "errors"
776 "fmt"
777 "io/ioutil"
778+ "net/url"
779 "os"
780 "os/exec"
781 "strings"
782
783 "launchpad.net/ubuntu-push/bus"
784 "launchpad.net/ubuntu-push/bus/connectivity"
785+ "launchpad.net/ubuntu-push/bus/emblemcounter"
786+ "launchpad.net/ubuntu-push/bus/haptic"
787 "launchpad.net/ubuntu-push/bus/networkmanager"
788 "launchpad.net/ubuntu-push/bus/notifications"
789 "launchpad.net/ubuntu-push/bus/systemimage"
790 "launchpad.net/ubuntu-push/bus/urldispatcher"
791+ "launchpad.net/ubuntu-push/click"
792 "launchpad.net/ubuntu-push/client/service"
793 "launchpad.net/ubuntu-push/client/session"
794 "launchpad.net/ubuntu-push/client/session/seenstate"
795@@ -68,6 +72,14 @@
796 LogLevel logger.ConfigLogLevel `json:"log_level"`
797 }
798
799+// PushService is the interface we use of service.PushService.
800+type PushService interface {
801+ // Start starts the service.
802+ Start() error
803+ // Unregister unregisters the token for appId.
804+ Unregister(appId string) error
805+}
806+
807 // PushClient is the Ubuntu Push Notifications client-side daemon.
808 type PushClient struct {
809 leveldbPath string
810@@ -80,6 +92,8 @@
811 notificationsEndp bus.Endpoint
812 urlDispatcherEndp bus.Endpoint
813 connectivityEndp bus.Endpoint
814+ emblemcounterEndp bus.Endpoint
815+ hapticEndp bus.Endpoint
816 systemImageEndp bus.Endpoint
817 systemImageInfo *systemimage.InfoResult
818 connCh chan bool
819@@ -88,9 +102,13 @@
820 session *session.ClientSession
821 sessionConnectedCh chan uint32
822 pushServiceEndpoint bus.Endpoint
823- pushService *service.PushService
824+ pushService PushService
825 postalServiceEndpoint bus.Endpoint
826 postalService *service.PostalService
827+ unregisterCh chan string
828+ trackAddressees map[string]bool
829+ clickUser *click.ClickUser
830+ hasPackage func(string) bool
831 }
832
833 // Creates a new Ubuntu Push Notifications client-side daemon that will use
834@@ -122,14 +140,26 @@
835 // later, we'll be specifying more logging options in the config file
836 client.log = logger.NewSimpleLogger(os.Stderr, client.config.LogLevel.Level())
837
838+ clickUser, err := click.User()
839+ if err != nil {
840+ return fmt.Errorf("libclick: %v", err)
841+ }
842+ client.clickUser = clickUser
843+ // overridden for testing
844+ client.hasPackage = clickUser.HasPackage
845+
846+ client.unregisterCh = make(chan string, 10)
847+
848 // overridden for testing
849 client.idder = identifier.New()
850 client.urlDispatcherEndp = bus.SessionBus.Endpoint(urldispatcher.BusAddress, client.log)
851 client.connectivityEndp = bus.SystemBus.Endpoint(networkmanager.BusAddress, client.log)
852 client.systemImageEndp = bus.SystemBus.Endpoint(systemimage.BusAddress, client.log)
853- if client.notificationsEndp == nil {
854- client.notificationsEndp = bus.SessionBus.Endpoint(notifications.BusAddress, client.log)
855- }
856+ client.notificationsEndp = bus.SessionBus.Endpoint(notifications.BusAddress, client.log)
857+ client.emblemcounterEndp = bus.SessionBus.Endpoint(emblemcounter.BusAddress, client.log)
858+ client.hapticEndp = bus.SessionBus.Endpoint(haptic.BusAddress, client.log)
859+ client.postalServiceEndpoint = bus.SessionBus.Endpoint(service.PostalServiceBusAddress, client.log)
860+ client.pushServiceEndpoint = bus.SessionBus.Endpoint(service.PushServiceBusAddress, client.log)
861
862 client.connCh = make(chan bool, 1)
863 client.sessionConnectedCh = make(chan uint32, 1)
864@@ -156,11 +186,25 @@
865 ExchangeTimeout: client.config.ExchangeTimeout.TimeDuration(),
866 HostsCachingExpiryTime: client.config.HostsCachingExpiryTime.TimeDuration(),
867 ExpectAllRepairedTime: client.config.ExpectAllRepairedTime.TimeDuration(),
868- PEM: client.pem,
869- Info: info,
870- AuthGetter: client.getAuthorization,
871- AuthURL: client.config.SessionURL,
872- }
873+ PEM: client.pem,
874+ Info: info,
875+ AuthGetter: client.getAuthorization,
876+ AuthURL: client.config.SessionURL,
877+ AddresseeChecker: client,
878+ }
879+}
880+
881+// derivePushServiceSetup derives the service setup from the client configuration bits.
882+func (client *PushClient) derivePushServiceSetup() (*service.PushServiceSetup, error) {
883+ setup := new(service.PushServiceSetup)
884+ purl, err := url.Parse(client.config.RegistrationURL)
885+ if err != nil {
886+ return nil, fmt.Errorf("cannot parse registration url: %v", err)
887+ }
888+ setup.RegURL = purl
889+ setup.DeviceId = client.deviceId
890+ setup.AuthGetter = client.getAuthorization
891+ return setup, nil
892 }
893
894 // getAuthorization gets the authorization blob to send to the server
895@@ -249,6 +293,42 @@
896 }
897 }
898
899+// StartAddresseeBatch starts a batch of checks for addressees.
900+func (client *PushClient) StartAddresseeBatch() {
901+ client.trackAddressees = make(map[string]bool, 10)
902+}
903+
904+// CheckForAddressee check for the addressee presence.
905+func (client *PushClient) CheckForAddressee(notif *protocol.Notification) bool {
906+ appId := notif.AppId
907+ present, ok := client.trackAddressees[appId]
908+ if ok {
909+ return present
910+ }
911+ present = client.hasPackage(appId)
912+ client.trackAddressees[appId] = present
913+ if !present {
914+ client.unregisterCh <- appId
915+ }
916+ return present
917+}
918+
919+// handleUnregister deals with tokens of uninstalled apps
920+func (client *PushClient) handleUnregister(appId string) {
921+ if !client.hasPackage(appId) {
922+ // xxx small chance of race here, in case the app gets
923+ // reinstalled and registers itself before we finish
924+ // the unregister; we need click and app launching
925+ // collaboration to do better. we redo the hasPackage
926+ // check here just before to keep the race window as
927+ // small as possible
928+ err := client.pushService.Unregister(appId)
929+ if err != nil {
930+ client.log.Errorf("unregistering %s: %s", appId, err)
931+ }
932+ }
933+}
934+
935 // handleConnState deals with connectivity events
936 func (client *PushClient) handleConnState(hasConnectivity bool) {
937 if client.hasConnectivity == hasConnectivity {
938@@ -320,9 +400,7 @@
939 if !client.filterBroadcastNotification(msg) {
940 return nil
941 }
942- not_id, err := client.postalService.SendNotification(service.ACTION_ID_BROADCAST,
943- "update_manager_icon", "There's an updated system image.",
944- "Tap to open the system updater.")
945+ not_id, err := client.postalService.InjectBroadcast()
946 if err != nil {
947 client.log.Errorf("showing notification: %s", err)
948 return err
949@@ -333,20 +411,30 @@
950
951 // handleUnicastNotification deals with receiving a unicast notification
952 func (client *PushClient) handleUnicastNotification(msg *protocol.Notification) error {
953+ appId, err := click.ParseAppId(msg.AppId)
954+ if err != nil {
955+ client.log.Debugf("notification %#v for invalid app id %#v.", msg.MsgId, msg.AppId)
956+ return errors.New("invalid app id in notification")
957+ }
958 client.log.Debugf("sending notification %#v for %#v.", msg.MsgId, msg.AppId)
959- return client.postalService.Inject(msg.AppId, msg.MsgId, string(msg.Payload))
960+ return client.postalService.Inject(appId.Package, msg.AppId, msg.MsgId, string(msg.Payload))
961 }
962
963 // handleClick deals with the user clicking a notification
964-func (client *PushClient) handleClick(action_id string) error {
965+func (client *PushClient) handleClick(actionId string) error {
966 // “The string is a stark data structure and everywhere it is passed
967 // there is much duplication of process. It is a perfect vehicle for
968 // hiding information.”
969 //
970 // From ACM's SIGPLAN publication, (September, 1982), Article
971 // "Epigrams in Programming", by Alan J. Perlis of Yale University.
972- url := strings.TrimPrefix(action_id, service.ACTION_ID_SNOWFLAKE)
973- if len(url) == len(action_id) || len(url) == 0 {
974+ url := actionId
975+ // XXX: branch for the broadcast notifications
976+ if strings.HasPrefix(actionId, service.ACTION_ID_PREFIX) {
977+ parts := strings.Split(actionId, "::")
978+ url = parts[1]
979+ }
980+ if len(url) == len(actionId) || len(url) == 0 {
981 // it didn't start with the prefix
982 return nil
983 }
984@@ -356,7 +444,7 @@
985 }
986
987 // doLoop connects events with their handlers
988-func (client *PushClient) doLoop(connhandler func(bool), clickhandler func(string) error, bcasthandler func(*session.BroadcastNotification) error, ucasthandler func(*protocol.Notification) error, errhandler func(error)) {
989+func (client *PushClient) doLoop(connhandler func(bool), clickhandler func(string) error, bcasthandler func(*session.BroadcastNotification) error, ucasthandler func(*protocol.Notification) error, errhandler func(error), unregisterhandler func(string)) {
990 for {
991 select {
992 case state := <-client.connCh:
993@@ -371,6 +459,8 @@
994 errhandler(err)
995 case count := <-client.sessionConnectedCh:
996 client.log.Debugf("Session connected after %d attempts", count)
997+ case appId := <-client.unregisterCh:
998+ unregisterhandler(appId)
999 }
1000 }
1001 }
1002@@ -392,21 +482,17 @@
1003 client.handleClick,
1004 client.handleBroadcastNotification,
1005 client.handleUnicastNotification,
1006- client.handleErr)
1007+ client.handleErr,
1008+ client.handleUnregister)
1009 }
1010
1011 func (client *PushClient) startService() error {
1012- if client.pushServiceEndpoint == nil {
1013- client.pushServiceEndpoint = bus.SessionBus.Endpoint(service.PushServiceBusAddress, client.log)
1014- }
1015- if client.postalServiceEndpoint == nil {
1016- client.postalServiceEndpoint = bus.SessionBus.Endpoint(service.PostalServiceBusAddress, client.log)
1017+ setup, err := client.derivePushServiceSetup()
1018+ if err != nil {
1019+ return err
1020 }
1021
1022- client.pushService = service.NewPushService(client.pushServiceEndpoint, client.log)
1023- client.pushService.SetRegistrationURL(client.config.RegistrationURL)
1024- client.pushService.SetAuthGetter(client.getAuthorization)
1025- client.pushService.SetDeviceId(client.deviceId)
1026+ client.pushService = service.NewPushService(client.pushServiceEndpoint, setup, client.log)
1027 if err := client.pushService.Start(); err != nil {
1028 return err
1029 }
1030@@ -414,10 +500,7 @@
1031 }
1032
1033 func (client *PushClient) setupPostalService() error {
1034- if client.notificationsEndp == nil {
1035- client.notificationsEndp = bus.SessionBus.Endpoint(notifications.BusAddress, client.log)
1036- }
1037- client.postalService = service.NewPostalService(client.postalServiceEndpoint, client.notificationsEndp, client.log)
1038+ client.postalService = service.NewPostalService(client.postalServiceEndpoint, client.notificationsEndp, client.emblemcounterEndp, client.hapticEndp, client.log)
1039 return nil
1040 }
1041
1042
1043=== modified file 'client/client_test.go'
1044--- client/client_test.go 2014-07-01 11:55:30 +0000
1045+++ client/client_test.go 2014-07-07 14:23:18 +0000
1046@@ -187,14 +187,26 @@
1047
1048 func (cs *clientSuite) TestConfigureSetsUpEndpoints(c *C) {
1049 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1050+
1051+ // keep these in the same order as in the client struct, for sanity
1052 c.Check(cli.notificationsEndp, IsNil)
1053 c.Check(cli.urlDispatcherEndp, IsNil)
1054 c.Check(cli.connectivityEndp, IsNil)
1055+ c.Check(cli.emblemcounterEndp, IsNil)
1056+ c.Check(cli.hapticEndp, IsNil)
1057+ c.Check(cli.systemImageEndp, IsNil)
1058+ c.Check(cli.pushServiceEndpoint, IsNil)
1059+ c.Check(cli.postalServiceEndpoint, IsNil)
1060 err := cli.configure()
1061 c.Assert(err, IsNil)
1062- c.Assert(cli.notificationsEndp, NotNil)
1063- c.Assert(cli.urlDispatcherEndp, NotNil)
1064- c.Assert(cli.connectivityEndp, NotNil)
1065+ c.Check(cli.notificationsEndp, NotNil)
1066+ c.Check(cli.urlDispatcherEndp, NotNil)
1067+ c.Check(cli.connectivityEndp, NotNil)
1068+ c.Check(cli.emblemcounterEndp, NotNil)
1069+ c.Check(cli.hapticEndp, NotNil)
1070+ c.Check(cli.systemImageEndp, NotNil)
1071+ c.Check(cli.pushServiceEndpoint, NotNil)
1072+ c.Check(cli.postalServiceEndpoint, NotNil)
1073 }
1074
1075 func (cs *clientSuite) TestConfigureSetsUpConnCh(c *C) {
1076@@ -205,6 +217,15 @@
1077 c.Assert(cli.connCh, NotNil)
1078 }
1079
1080+func (cs *clientSuite) TestConfigureSetsUpAddresseeChecks(c *C) {
1081+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1082+ c.Check(cli.unregisterCh, IsNil)
1083+ err := cli.configure()
1084+ c.Assert(err, IsNil)
1085+ c.Assert(cli.unregisterCh, NotNil)
1086+ c.Assert(cli.hasPackage("com.bar.baz_foo"), Equals, false)
1087+}
1088+
1089 func (cs *clientSuite) TestConfigureBailsOnBadFilename(c *C) {
1090 cli := NewPushClient("/does/not/exist", cs.leveldbPath)
1091 err := cli.configure()
1092@@ -255,6 +276,35 @@
1093 }
1094
1095 /*****************************************************************
1096+ addresses checking tests
1097+******************************************************************/
1098+
1099+func (cs *clientSuite) TestCheckForAddressee(c *C) {
1100+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1101+ cli.unregisterCh = make(chan string, 5)
1102+ cli.StartAddresseeBatch()
1103+ calls := 0
1104+ cli.hasPackage = func(appId string) bool {
1105+ calls++
1106+ if appId == "app1" {
1107+ return false
1108+ }
1109+ return true
1110+ }
1111+ c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: "app1"}), Equals, false)
1112+ c.Check(calls, Equals, 1)
1113+ c.Assert(cli.unregisterCh, HasLen, 1)
1114+ c.Check(<-cli.unregisterCh, Equals, "app1")
1115+ c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: "app2"}), Equals, true)
1116+ c.Check(calls, Equals, 2)
1117+ c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: "app1"}), Equals, false)
1118+ c.Check(calls, Equals, 2)
1119+ c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: "app2"}), Equals, true)
1120+ c.Check(calls, Equals, 2)
1121+ c.Check(cli.unregisterCh, HasLen, 0)
1122+}
1123+
1124+/*****************************************************************
1125 deriveSessionConfig tests
1126 ******************************************************************/
1127
1128@@ -273,10 +323,11 @@
1129 ExchangeTimeout: 10 * time.Millisecond,
1130 HostsCachingExpiryTime: 1 * time.Hour,
1131 ExpectAllRepairedTime: 30 * time.Minute,
1132- PEM: cli.pem,
1133- Info: info,
1134- AuthGetter: func(string) string { return "" },
1135- AuthURL: "xyzzy://",
1136+ PEM: cli.pem,
1137+ Info: info,
1138+ AuthGetter: func(string) string { return "" },
1139+ AuthURL: "xyzzy://",
1140+ AddresseeChecker: cli,
1141 }
1142 // sanity check that we are looking at all fields
1143 vExpected := reflect.ValueOf(expected)
1144@@ -289,7 +340,7 @@
1145 // finally compare
1146 conf := cli.deriveSessionConfig(info)
1147 // compare authGetter by string
1148- c.Check(fmt.Sprintf("%v", conf.AuthGetter), Equals, fmt.Sprintf("%v", cli.getAuthorization))
1149+ c.Check(fmt.Sprintf("%#v", conf.AuthGetter), Equals, fmt.Sprintf("%#v", cli.getAuthorization))
1150 // and set it to nil
1151 conf.AuthGetter = nil
1152 expected.AuthGetter = nil
1153@@ -297,6 +348,51 @@
1154 }
1155
1156 /*****************************************************************
1157+ derivePushServiceSetup tests
1158+******************************************************************/
1159+
1160+func (cs *clientSuite) TestDerivePushServiceSetup(c *C) {
1161+ cs.writeTestConfig(map[string]interface{}{})
1162+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1163+ err := cli.configure()
1164+ c.Assert(err, IsNil)
1165+ cli.deviceId = "zoo"
1166+ expected := &service.PushServiceSetup{
1167+ DeviceId: "zoo",
1168+ AuthGetter: func(string) string { return "" },
1169+ RegURL: helpers.ParseURL("reg://"),
1170+ }
1171+ // sanity check that we are looking at all fields
1172+ vExpected := reflect.ValueOf(expected).Elem()
1173+ nf := vExpected.NumField()
1174+ for i := 0; i < nf; i++ {
1175+ fv := vExpected.Field(i)
1176+ // field isn't empty/zero
1177+ c.Assert(fv.Interface(), Not(DeepEquals), reflect.Zero(fv.Type()).Interface(), Commentf("forgot about: %s", vExpected.Type().Field(i).Name))
1178+ }
1179+ // finally compare
1180+ setup, err := cli.derivePushServiceSetup()
1181+ c.Assert(err, IsNil)
1182+ // compare authGetter by string
1183+ c.Check(fmt.Sprintf("%#v", setup.AuthGetter), Equals, fmt.Sprintf("%#v", cli.getAuthorization))
1184+ // and set it to nil
1185+ setup.AuthGetter = nil
1186+ expected.AuthGetter = nil
1187+ c.Check(setup, DeepEquals, expected)
1188+}
1189+
1190+func (cs *clientSuite) TestDerivePushServiceSetupError(c *C) {
1191+ cs.writeTestConfig(map[string]interface{}{
1192+ "registration_url": "%gh",
1193+ })
1194+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1195+ err := cli.configure()
1196+ c.Assert(err, IsNil)
1197+ _, err = cli.derivePushServiceSetup()
1198+ c.Check(err, ErrorMatches, "cannot parse registration url:.*")
1199+}
1200+
1201+/*****************************************************************
1202 startService tests
1203 ******************************************************************/
1204
1205@@ -305,7 +401,8 @@
1206 "auth_helper": helpers.ScriptAbsPath("dummyauth.sh"),
1207 })
1208 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1209- cli.configure()
1210+ err := cli.configure()
1211+ c.Assert(err, IsNil)
1212 cli.log = cs.log
1213 cli.deviceId = "fake-id"
1214 cli.pushServiceEndpoint = testibus.NewTestingEndpoint(condition.Work(true), nil)
1215@@ -313,16 +410,26 @@
1216 c.Check(cli.pushService, IsNil)
1217 c.Check(cli.startService(), IsNil)
1218 c.Assert(cli.pushService, NotNil)
1219- c.Check(cli.pushService.IsRunning(), Equals, true)
1220- c.Check(cli.pushService.GetDeviceId(), Equals, "fake-id")
1221- c.Check(cli.pushService.GetRegistrationAuthorization(), Equals, "hello reg://")
1222+ pushService := cli.pushService.(*service.PushService)
1223+ c.Check(pushService.IsRunning(), Equals, true)
1224 c.Assert(cli.setupPostalService(), IsNil)
1225 c.Assert(cli.startPostalService(), IsNil)
1226 c.Check(cli.postalService.IsRunning(), Equals, true)
1227- cli.pushService.Stop()
1228+ pushService.Stop()
1229 cli.postalService.Stop()
1230 }
1231
1232+func (cs *clientSuite) TestStartServiceSetupError(c *C) {
1233+ cs.writeTestConfig(map[string]interface{}{
1234+ "registration_url": "%gh",
1235+ })
1236+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1237+ err := cli.configure()
1238+ c.Assert(err, IsNil)
1239+ err = cli.startService()
1240+ c.Check(err, ErrorMatches, "cannot parse registration url:.*")
1241+}
1242+
1243 func (cs *clientSuite) TestStartServiceErrorsOnNilLog(c *C) {
1244 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1245 c.Check(cli.log, IsNil)
1246@@ -404,6 +511,10 @@
1247 siCond := condition.Fail2Work(2)
1248 siEndp := testibus.NewMultiValuedTestingEndpoint(siCond, condition.Work(true), []interface{}{int32(101), "mako", "daily", "Unknown", map[string]string{}})
1249 testibus.SetWatchTicker(cEndp, make(chan bool))
1250+ ecCond := condition.Fail2Work(13)
1251+ ecEndp := testibus.NewTestingEndpoint(ecCond, condition.Work(true))
1252+ haCond := condition.Fail2Work(2)
1253+ haEndp := testibus.NewTestingEndpoint(haCond, condition.Work(true))
1254 // ok, create the thing
1255 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1256 cli.log = cs.log
1257@@ -418,6 +529,8 @@
1258 cli.notificationsEndp = nEndp
1259 cli.urlDispatcherEndp = uEndp
1260 cli.connectivityEndp = cEndp
1261+ cli.emblemcounterEndp = ecEndp
1262+ cli.hapticEndp = haEndp
1263 cli.systemImageEndp = siEndp
1264
1265 c.Assert(cli.takeTheBus(), IsNil)
1266@@ -434,6 +547,10 @@
1267 c.Check(cCond.OK(), Equals, true)
1268 // the systemimage endpoint retried until connected
1269 c.Check(siCond.OK(), Equals, true)
1270+ // the emblemcounter endpoint retried until connected
1271+ c.Check(ecCond.OK(), Equals, true)
1272+ // the haptic endpoint retried until connected
1273+ c.Check(haCond.OK(), Equals, true)
1274 }
1275
1276 // takeTheBus can, in fact, fail
1277@@ -449,6 +566,8 @@
1278 cli.notificationsEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
1279 cli.urlDispatcherEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
1280 cli.connectivityEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
1281+ cli.emblemcounterEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
1282+ cli.hapticEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
1283 cli.systemImageEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
1284
1285 c.Check(cli.takeTheBus(), NotNil)
1286@@ -654,7 +773,7 @@
1287 args := testibus.GetCallArgs(endp)
1288 c.Assert(args, HasLen, 1)
1289 c.Check(args[0].Member, Equals, "Notify")
1290- c.Check(cs.log.Captured(), Matches, `.* got notification id \d+\s*`)
1291+ c.Check(cs.log.Captured(), Matches, `(?s).* got notification id \d+\s*`)
1292 }
1293
1294 func (cs *clientSuite) TestHandleBroadcastNotificationNothingToDo(c *C) {
1295@@ -686,18 +805,13 @@
1296 ******************************************************************/
1297
1298 var payload = `{"message": "aGVsbG8=", "notification": {"card": {"icon": "icon-value", "summary": "summary-value", "body": "body-value", "actions": []}}}`
1299-var notif = &protocol.Notification{AppId: "hello", Payload: []byte(payload), MsgId: "42"}
1300+var notif = &protocol.Notification{AppId: "com.example.test_hello", Payload: []byte(payload), MsgId: "42"}
1301
1302 func (cs *clientSuite) TestHandleUcastNotification(c *C) {
1303 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1304- svcEndp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(1))
1305 postEndp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(1))
1306 cli.log = cs.log
1307- cli.pushServiceEndpoint = svcEndp
1308 cli.postalServiceEndpoint = postEndp
1309- notsEndp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
1310- cli.notificationsEndp = notsEndp
1311- c.Assert(cli.startService(), IsNil)
1312 c.Assert(cli.setupPostalService(), IsNil)
1313 c.Assert(cli.startPostalService(), IsNil)
1314 c.Check(cli.handleUnicastNotification(notif), IsNil)
1315@@ -705,13 +819,22 @@
1316 args := testibus.GetCallArgs(postEndp)
1317 c.Assert(len(args), Not(Equals), 0)
1318 c.Check(args[len(args)-1].Member, Equals, "::Signal")
1319- c.Check(cs.log.Captured(), Matches, `(?m).*sending notification "42" for "hello".*`)
1320+ c.Check(cs.log.Captured(), Matches, `(?m).*sending notification "42" for "com.example.test_hello".*`)
1321+}
1322+
1323+func (cs *clientSuite) TestHandleUcastFailsOnBadAppId(c *C) {
1324+ notif := &protocol.Notification{AppId: "bad-app-id", MsgId: "-1"}
1325+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1326+ cli.log = cs.log
1327+ c.Check(cli.handleUnicastNotification(notif), ErrorMatches, "invalid app id in notification")
1328 }
1329
1330 /*****************************************************************
1331 handleClick tests
1332 ******************************************************************/
1333
1334+var ACTION_ID_BROADCAST = service.ACTION_ID_PREFIX + service.SystemUpdateUrl + service.ACTION_ID_SUFFIX
1335+
1336 func (cs *clientSuite) TestHandleClick(c *C) {
1337 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1338 cli.log = cs.log
1339@@ -723,14 +846,14 @@
1340 args := testibus.GetCallArgs(endp)
1341 c.Assert(args, HasLen, 0)
1342 // check we worked with the right action id
1343- c.Check(cli.handleClick(service.ACTION_ID_BROADCAST), IsNil)
1344+ c.Check(cli.handleClick(ACTION_ID_BROADCAST), IsNil)
1345 // check we sent the notification
1346 args = testibus.GetCallArgs(endp)
1347 c.Assert(args, HasLen, 1)
1348 c.Check(args[0].Member, Equals, "DispatchURL")
1349 c.Check(args[0].Args, DeepEquals, []interface{}{service.SystemUpdateUrl})
1350 // check we worked with the right action id
1351- c.Check(cli.handleClick(service.ACTION_ID_SNOWFLAKE+"foo"), IsNil)
1352+ c.Check(cli.handleClick(service.ACTION_ID_PREFIX+"foo"), IsNil)
1353 // check we sent the notification
1354 args = testibus.GetCallArgs(endp)
1355 c.Assert(args, HasLen, 2)
1356@@ -739,6 +862,64 @@
1357 }
1358
1359 /*****************************************************************
1360+ handleUnregister tests
1361+******************************************************************/
1362+
1363+type testPushService struct {
1364+ err error
1365+ unregistered string
1366+}
1367+
1368+func (ps *testPushService) Start() error {
1369+ return nil
1370+}
1371+
1372+func (ps *testPushService) Unregister(appId string) error {
1373+ ps.unregistered = appId
1374+ return ps.err
1375+}
1376+
1377+func (cs *clientSuite) TestHandleUnregister(c *C) {
1378+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1379+ cli.log = cs.log
1380+ cli.hasPackage = func(appId string) bool {
1381+ c.Check(appId, Equals, "app1")
1382+ return false
1383+ }
1384+ ps := &testPushService{}
1385+ cli.pushService = ps
1386+ cli.handleUnregister("app1")
1387+ c.Assert(ps.unregistered, Equals, "app1")
1388+ c.Check(cs.log.Captured(), Equals, "")
1389+}
1390+
1391+func (cs *clientSuite) TestHandleUnregisterNop(c *C) {
1392+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1393+ cli.log = cs.log
1394+ cli.hasPackage = func(appId string) bool {
1395+ c.Check(appId, Equals, "app1")
1396+ return true
1397+ }
1398+ ps := &testPushService{}
1399+ cli.pushService = ps
1400+ cli.handleUnregister("app1")
1401+ c.Assert(ps.unregistered, Equals, "")
1402+}
1403+
1404+func (cs *clientSuite) TestHandleUnregisterError(c *C) {
1405+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1406+ cli.log = cs.log
1407+ cli.hasPackage = func(appId string) bool {
1408+ return false
1409+ }
1410+ fail := errors.New("BAD")
1411+ ps := &testPushService{err: fail}
1412+ cli.pushService = ps
1413+ cli.handleUnregister("app1")
1414+ c.Check(cs.log.Captured(), Matches, "ERROR unregistering app1: BAD\n")
1415+}
1416+
1417+/*****************************************************************
1418 doLoop tests
1419 ******************************************************************/
1420
1421@@ -747,6 +928,7 @@
1422 var nopBcast = func(*session.BroadcastNotification) error { return nil }
1423 var nopUcast = func(*protocol.Notification) error { return nil }
1424 var nopError = func(error) {}
1425+var nopUnregister = func(string) {}
1426
1427 func (cs *clientSuite) TestDoLoopConn(c *C) {
1428 cli := NewPushClient(cs.configPath, cs.leveldbPath)
1429@@ -757,7 +939,7 @@
1430 c.Assert(cli.initSession(), IsNil)
1431
1432 ch := make(chan bool, 1)
1433- go cli.doLoop(func(bool) { ch <- true }, nopClick, nopBcast, nopUcast, nopError)
1434+ go cli.doLoop(func(bool) { ch <- true }, nopClick, nopBcast, nopUcast, nopError, nopUnregister)
1435 c.Check(takeNextBool(ch), Equals, true)
1436 }
1437
1438@@ -771,7 +953,7 @@
1439 cli.actionsCh = aCh
1440
1441 ch := make(chan bool, 1)
1442- go cli.doLoop(nopConn, func(_ string) error { ch <- true; return nil }, nopBcast, nopUcast, nopError)
1443+ go cli.doLoop(nopConn, func(_ string) error { ch <- true; return nil }, nopBcast, nopUcast, nopError, nopUnregister)
1444 c.Check(takeNextBool(ch), Equals, true)
1445 }
1446
1447@@ -784,7 +966,7 @@
1448 cli.session.BroadcastCh <- &session.BroadcastNotification{}
1449
1450 ch := make(chan bool, 1)
1451- go cli.doLoop(nopConn, nopClick, func(_ *session.BroadcastNotification) error { ch <- true; return nil }, nopUcast, nopError)
1452+ go cli.doLoop(nopConn, nopClick, func(_ *session.BroadcastNotification) error { ch <- true; return nil }, nopUcast, nopError, nopUnregister)
1453 c.Check(takeNextBool(ch), Equals, true)
1454 }
1455
1456@@ -797,7 +979,7 @@
1457 cli.session.NotificationsCh <- &protocol.Notification{}
1458
1459 ch := make(chan bool, 1)
1460- go cli.doLoop(nopConn, nopClick, nopBcast, func(*protocol.Notification) error { ch <- true; return nil }, nopError)
1461+ go cli.doLoop(nopConn, nopClick, nopBcast, func(*protocol.Notification) error { ch <- true; return nil }, nopError, nopUnregister)
1462 c.Check(takeNextBool(ch), Equals, true)
1463 }
1464
1465@@ -810,7 +992,20 @@
1466 cli.session.ErrCh <- nil
1467
1468 ch := make(chan bool, 1)
1469- go cli.doLoop(nopConn, nopClick, nopBcast, nopUcast, func(error) { ch <- true })
1470+ go cli.doLoop(nopConn, nopClick, nopBcast, nopUcast, func(error) { ch <- true }, nopUnregister)
1471+ c.Check(takeNextBool(ch), Equals, true)
1472+}
1473+
1474+func (cs *clientSuite) TestDoLoopUnregister(c *C) {
1475+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
1476+ cli.log = cs.log
1477+ cli.systemImageInfo = siInfoRes
1478+ c.Assert(cli.initSession(), IsNil)
1479+ cli.unregisterCh = make(chan string, 1)
1480+ cli.unregisterCh <- "app1"
1481+
1482+ ch := make(chan bool, 1)
1483+ go cli.doLoop(nopConn, nopClick, nopBcast, nopUcast, nopError, func(appId string) { c.Check(appId, Equals, "app1"); ch <- true })
1484 c.Check(takeNextBool(ch), Equals, true)
1485 }
1486
1487@@ -879,7 +1074,7 @@
1488 c.Check(cs.log.Captured(), Matches, "(?ms).*Session connected after 42 attempts$")
1489
1490 // * actionsCh to the click handler/url dispatcher
1491- aCh <- notifications.RawActionReply{ActionId: service.ACTION_ID_BROADCAST}
1492+ aCh <- notifications.RawActionReply{ActionId: ACTION_ID_BROADCAST}
1493 tick()
1494 uargs := testibus.GetCallArgs(cli.urlDispatcherEndp)
1495 c.Assert(uargs, HasLen, 1)
1496@@ -943,7 +1138,7 @@
1497 // so we start,
1498 err := cli.Start()
1499 // and it works
1500- c.Check(err, IsNil)
1501+ c.Assert(err, IsNil)
1502
1503 // and now everthing is better! We have a config,
1504 c.Check(string(cli.config.Addr), Equals, ":0")
1505@@ -956,8 +1151,8 @@
1506 // and a service,
1507 c.Check(cli.pushService, NotNil)
1508 // and everthying us just peachy!
1509- cli.pushService.Stop() // cleanup
1510- cli.postalService.Stop() // cleanup
1511+ cli.pushService.(*service.PushService).Stop() // cleanup
1512+ cli.postalService.Stop() // cleanup
1513 }
1514
1515 func (cs *clientSuite) TestStartCanFail(c *C) {
1516
1517=== modified file 'client/service/common.go'
1518--- client/service/common.go 2014-07-01 11:55:30 +0000
1519+++ client/service/common.go 2014-07-07 14:23:18 +0000
1520@@ -20,10 +20,13 @@
1521
1522 import (
1523 "errors"
1524+ "strings"
1525 "sync"
1526
1527 "launchpad.net/ubuntu-push/bus"
1528+ "launchpad.net/ubuntu-push/click"
1529 "launchpad.net/ubuntu-push/logger"
1530+ "launchpad.net/ubuntu-push/nih"
1531 )
1532
1533 type DBusService struct {
1534@@ -43,10 +46,11 @@
1535 )
1536
1537 var (
1538- NotConfigured = errors.New("not configured")
1539- AlreadyStarted = errors.New("already started")
1540- BadArgCount = errors.New("Wrong number of arguments")
1541- BadArgType = errors.New("Bad argument type")
1542+ ErrNotConfigured = errors.New("not configured")
1543+ ErrAlreadyStarted = errors.New("already started")
1544+ ErrBadArgCount = errors.New("wrong number of arguments")
1545+ ErrBadArgType = errors.New("bad argument type")
1546+ ErrBadAppId = errors.New("package must be prefix of app id")
1547 )
1548
1549 // IsRunning() returns whether the service's state is StateRunning
1550@@ -61,10 +65,10 @@
1551 svc.lock.Lock()
1552 defer svc.lock.Unlock()
1553 if svc.state != StateUnknown {
1554- return AlreadyStarted
1555+ return ErrAlreadyStarted
1556 }
1557 if svc.Log == nil || svc.Bus == nil {
1558- return NotConfigured
1559+ return ErrNotConfigured
1560 }
1561 err := svc.Bus.Dial()
1562 if err != nil {
1563@@ -97,3 +101,21 @@
1564 }
1565 svc.state = StateFinished
1566 }
1567+
1568+// grabDBusPackageAndAppId() extracts the appId from a dbus-provided
1569+// []interface{}, and checks it against the package in the last
1570+// element of the dbus path.
1571+func grabDBusPackageAndAppId(path string, args []interface{}, numExtra int) (pkgname string, appId string, err error) {
1572+ if len(args) != 1+numExtra {
1573+ return "", "", ErrBadArgCount
1574+ }
1575+ appId, ok := args[0].(string)
1576+ if !ok {
1577+ return "", "", ErrBadArgType
1578+ }
1579+ pkgname = string(nih.Unquote([]byte(path[strings.LastIndex(path, "/")+1:])))
1580+ if !click.AppInPackage(appId, pkgname) {
1581+ return "", "", ErrBadAppId
1582+ }
1583+ return
1584+}
1585
1586=== added file 'client/service/common_test.go'
1587--- client/service/common_test.go 1970-01-01 00:00:00 +0000
1588+++ client/service/common_test.go 2014-07-07 14:23:18 +0000
1589@@ -0,0 +1,60 @@
1590+/*
1591+ Copyright 2014 Canonical Ltd.
1592+
1593+ This program is free software: you can redistribute it and/or modify it
1594+ under the terms of the GNU General Public License version 3, as published
1595+ by the Free Software Foundation.
1596+
1597+ This program is distributed in the hope that it will be useful, but
1598+ WITHOUT ANY WARRANTY; without even the implied warranties of
1599+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1600+ PURPOSE. See the GNU General Public License for more details.
1601+
1602+ You should have received a copy of the GNU General Public License along
1603+ with this program. If not, see <http://www.gnu.org/licenses/>.
1604+*/
1605+
1606+package service
1607+
1608+import (
1609+ . "launchpad.net/gocheck"
1610+)
1611+
1612+type commonSuite struct{}
1613+
1614+var _ = Suite(&commonSuite{})
1615+
1616+func (cs *commonSuite) TestGrabDBusPackageAndAppIdWorks(c *C) {
1617+ aDBusPath := "/com/ubuntu/Postal/com_2eexample_2etest"
1618+ aPackage := "com.example.test"
1619+ anAppId := aPackage + "_test"
1620+ pkg, app, err := grabDBusPackageAndAppId(aDBusPath, []interface{}{anAppId}, 0)
1621+ c.Check(err, IsNil)
1622+ c.Check(pkg, Equals, aPackage)
1623+ c.Check(app, Equals, anAppId)
1624+}
1625+
1626+func (cs *commonSuite) TestGrabDBusPackageAndAppIdFails(c *C) {
1627+ aDBusPath := "/com/ubuntu/Postal/com_2eexample_2etest"
1628+ aPackage := "com.example.test"
1629+ anAppId := aPackage + "_test"
1630+
1631+ for i, s := range []struct {
1632+ path string
1633+ args []interface{}
1634+ numExtra int
1635+ errt error
1636+ }{
1637+ {aDBusPath, []interface{}{}, 0, ErrBadArgCount},
1638+ {aDBusPath, []interface{}{anAppId}, 1, ErrBadArgCount},
1639+ {aDBusPath, []interface{}{anAppId, anAppId}, 0, ErrBadArgCount},
1640+ {aDBusPath, []interface{}{1}, 0, ErrBadArgType},
1641+ {aDBusPath, []interface{}{aPackage}, 0, ErrBadAppId},
1642+ } {
1643+ comment := Commentf("iteration #%d", i)
1644+ pkg, app, err := grabDBusPackageAndAppId(s.path, s.args, s.numExtra)
1645+ c.Check(err, Equals, s.errt, comment)
1646+ c.Check(pkg, Equals, "", comment)
1647+ c.Check(app, Equals, "", comment)
1648+ }
1649+}
1650
1651=== modified file 'client/service/postal.go'
1652--- client/service/postal.go 2014-07-01 11:55:30 +0000
1653+++ client/service/postal.go 2014-07-07 14:23:18 +0000
1654@@ -17,17 +17,19 @@
1655 package service
1656
1657 import (
1658- "strings"
1659+ "sync"
1660
1661 "code.google.com/p/go-uuid/uuid"
1662- "launchpad.net/go-dbus/v1"
1663
1664 "launchpad.net/ubuntu-push/bus"
1665+ "launchpad.net/ubuntu-push/bus/emblemcounter"
1666+ "launchpad.net/ubuntu-push/bus/haptic"
1667 "launchpad.net/ubuntu-push/bus/notifications"
1668 "launchpad.net/ubuntu-push/launch_helper"
1669 "launchpad.net/ubuntu-push/logger"
1670 "launchpad.net/ubuntu-push/messaging"
1671 "launchpad.net/ubuntu-push/nih"
1672+ "launchpad.net/ubuntu-push/sounds"
1673 "launchpad.net/ubuntu-push/util"
1674 )
1675
1676@@ -38,6 +40,8 @@
1677 msgHandler func(string, string, *launch_helper.HelperOutput) error
1678 HelperLauncher launch_helper.HelperLauncher
1679 messagingMenu *messaging.MessagingMenu
1680+ emblemcounterEndp bus.Endpoint
1681+ hapticEndp bus.Endpoint
1682 notificationsEndp bus.Endpoint
1683 }
1684
1685@@ -50,19 +54,21 @@
1686 )
1687
1688 var (
1689- SystemUpdateUrl = "settings:///system/system-update"
1690- ACTION_ID_SNOWFLAKE = "::ubuntu-push-client::"
1691- ACTION_ID_BROADCAST = ACTION_ID_SNOWFLAKE + SystemUpdateUrl
1692+ SystemUpdateUrl = "settings:///system/system-update"
1693+ ACTION_ID_PREFIX = "ubuntu-push-client::"
1694+ ACTION_ID_SUFFIX = "::0"
1695 )
1696
1697 // NewPostalService() builds a new service and returns it.
1698-func NewPostalService(busEndp bus.Endpoint, notificationsEndp bus.Endpoint, log logger.Logger) *PostalService {
1699+func NewPostalService(busEndp bus.Endpoint, notificationsEndp bus.Endpoint, emblemcounterEndp bus.Endpoint, hapticEndp bus.Endpoint, log logger.Logger) *PostalService {
1700 var svc = &PostalService{}
1701 svc.Log = log
1702 svc.Bus = busEndp
1703 svc.messagingMenu = messaging.New(log)
1704 svc.HelperLauncher = launch_helper.NewTrivialHelperLauncher(log)
1705 svc.notificationsEndp = notificationsEndp
1706+ svc.emblemcounterEndp = emblemcounterEndp
1707+ svc.hapticEndp = hapticEndp
1708 svc.msgHandler = svc.messageHandler
1709 return svc
1710 }
1711@@ -84,24 +90,36 @@
1712 // Start() dials the bus, grab the name, and listens for method calls.
1713 func (svc *PostalService) Start() error {
1714 return svc.DBusService.Start(bus.DispatchMap{
1715- "Notifications": svc.notifications,
1716- "Inject": svc.inject,
1717+ "Messages": svc.notifications,
1718+ "Post": svc.inject,
1719 }, PostalServiceBusAddress)
1720 }
1721
1722 func (svc *PostalService) TakeTheBus() (<-chan notifications.RawActionReply, error) {
1723- iniCh := make(chan uint32)
1724- go func() { iniCh <- util.NewAutoRedialer(svc.notificationsEndp).Redial() }()
1725- <-iniCh
1726+ var wg sync.WaitGroup
1727+ endps := []bus.Endpoint{
1728+ svc.notificationsEndp,
1729+ svc.emblemcounterEndp,
1730+ svc.hapticEndp,
1731+ }
1732+ wg.Add(len(endps))
1733+ for _, endp := range endps {
1734+ go func(endp bus.Endpoint) {
1735+ util.NewAutoRedialer(endp).Redial()
1736+ wg.Done()
1737+ }(endp)
1738+ }
1739+ wg.Wait()
1740 actionsCh, err := notifications.Raw(svc.notificationsEndp, svc.Log).WatchActions()
1741+
1742 return actionsCh, err
1743 }
1744
1745 func (svc *PostalService) notifications(path string, args, _ []interface{}) ([]interface{}, error) {
1746- if len(args) != 0 {
1747- return nil, BadArgCount
1748+ _, appId, err := grabDBusPackageAndAppId(path, args, 0)
1749+ if err != nil {
1750+ return nil, err
1751 }
1752- appname := string(nih.Unquote([]byte(path[strings.LastIndex(path, "/")+1:])))
1753
1754 svc.lock.Lock()
1755 defer svc.lock.Unlock()
1756@@ -109,8 +127,8 @@
1757 if svc.mbox == nil {
1758 return []interface{}{[]string(nil)}, nil
1759 }
1760- msgs := svc.mbox[appname]
1761- delete(svc.mbox, appname)
1762+ msgs := svc.mbox[appId]
1763+ delete(svc.mbox, appId)
1764
1765 return []interface{}{msgs}, nil
1766 }
1767@@ -118,23 +136,23 @@
1768 var newNid = uuid.New
1769
1770 func (svc *PostalService) inject(path string, args, _ []interface{}) ([]interface{}, error) {
1771- if len(args) != 1 {
1772- return nil, BadArgCount
1773+ pkg, appId, err := grabDBusPackageAndAppId(path, args, 1)
1774+ if err != nil {
1775+ return nil, err
1776 }
1777- notif, ok := args[0].(string)
1778+ notif, ok := args[1].(string)
1779 if !ok {
1780- return nil, BadArgType
1781+ return nil, ErrBadArgType
1782 }
1783- appname := string(nih.Unquote([]byte(path[strings.LastIndex(path, "/")+1:])))
1784
1785 nid := newNid()
1786
1787- return nil, svc.Inject(appname, nid, notif)
1788+ return nil, svc.Inject(pkg, appId, nid, notif)
1789 }
1790
1791 // Inject() signals to an application over dbus that a notification
1792 // has arrived.
1793-func (svc *PostalService) Inject(appname string, nid string, notif string) error {
1794+func (svc *PostalService) Inject(pkgname string, appname string, nid string, notif string) error {
1795 svc.lock.Lock()
1796 defer svc.lock.Unlock()
1797 if svc.mbox == nil {
1798@@ -153,29 +171,28 @@
1799 svc.DBusService.Log.Debugf("call to msgHandler successful")
1800 }
1801
1802- return svc.Bus.Signal("Notification", "/"+string(nih.Quote([]byte(appname))), []interface{}{appname})
1803+ return svc.Bus.Signal("Post", "/"+string(nih.Quote([]byte(pkgname))), []interface{}{appname})
1804 }
1805
1806 func (svc *PostalService) messageHandler(appname string, nid string, output *launch_helper.HelperOutput) error {
1807 svc.messagingMenu.Present(appname, nid, output.Notification)
1808 nots := notifications.Raw(svc.notificationsEndp, svc.Log)
1809 _, err := nots.Present(appname, nid, output.Notification)
1810+ emblemcounter.New(svc.emblemcounterEndp, svc.Log).Present(appname, nid, output.Notification)
1811+ haptic.New(svc.hapticEndp, svc.Log).Present(appname, nid, output.Notification)
1812+ sounds.New(svc.Log).Present(appname, nid, output.Notification)
1813
1814 return err
1815 }
1816
1817-func (svc *PostalService) SendNotification(action_id, icon, summary, body string) (uint32, error) {
1818- a := []string{action_id, "Switch to app"} // action value not visible on the phone
1819- h := map[string]*dbus.Variant{"x-canonical-switch-to-application": &dbus.Variant{true}}
1820- nots := notifications.Raw(svc.notificationsEndp, svc.Log)
1821- return nots.Notify(
1822- "ubuntu-push-client", // app name
1823- uint32(0), // id
1824- icon, // icon
1825- summary, // summary
1826- body, // body
1827- a, // actions
1828- h, // hints
1829- int32(10*1000), // timeout (ms)
1830- )
1831+func (svc *PostalService) InjectBroadcast() (uint32, error) {
1832+ // XXX: call a helper?
1833+ // XXX: Present force us to send the url as the notificationId
1834+ icon := "update_manager_icon"
1835+ summary := "There's an updated system image."
1836+ body := "Tap to open the system updater."
1837+ actions := []string{"Switch to app"} // action value not visible on the phone
1838+ card := &launch_helper.Card{Icon: icon, Summary: summary, Body: body, Actions: actions, Popup: true}
1839+ output := &launch_helper.HelperOutput{[]byte(""), &launch_helper.Notification{Card: card}}
1840+ return 0, svc.msgHandler("ubuntu-push-client", SystemUpdateUrl, output)
1841 }
1842
1843=== modified file 'client/service/postal_test.go'
1844--- client/service/postal_test.go 2014-07-01 11:55:30 +0000
1845+++ client/service/postal_test.go 2014-07-07 14:23:18 +0000
1846@@ -18,6 +18,7 @@
1847
1848 import (
1849 "errors"
1850+ "sort"
1851
1852 . "launchpad.net/gocheck"
1853
1854@@ -29,9 +30,11 @@
1855 )
1856
1857 type postalSuite struct {
1858- log *helpers.TestLogger
1859- bus bus.Endpoint
1860- notifBus bus.Endpoint
1861+ log *helpers.TestLogger
1862+ bus bus.Endpoint
1863+ notifBus bus.Endpoint
1864+ counterBus bus.Endpoint
1865+ hapticBus bus.Endpoint
1866 }
1867
1868 var _ = Suite(&postalSuite{})
1869@@ -40,10 +43,12 @@
1870 ss.log = helpers.NewTestLogger(c, "debug")
1871 ss.bus = testibus.NewTestingEndpoint(condition.Work(true), nil)
1872 ss.notifBus = testibus.NewTestingEndpoint(condition.Work(true), nil)
1873+ ss.counterBus = testibus.NewTestingEndpoint(condition.Work(true), nil)
1874+ ss.hapticBus = testibus.NewTestingEndpoint(condition.Work(true), nil)
1875 }
1876
1877 func (ss *postalSuite) TestStart(c *C) {
1878- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
1879+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1880 c.Check(svc.IsRunning(), Equals, false)
1881 c.Check(svc.Start(), IsNil)
1882 c.Check(svc.IsRunning(), Equals, true)
1883@@ -51,45 +56,45 @@
1884 }
1885
1886 func (ss *postalSuite) TestStartTwice(c *C) {
1887- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
1888+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1889 c.Check(svc.Start(), IsNil)
1890- c.Check(svc.Start(), Equals, AlreadyStarted)
1891+ c.Check(svc.Start(), Equals, ErrAlreadyStarted)
1892 svc.Stop()
1893 }
1894
1895 func (ss *postalSuite) TestStartNoLog(c *C) {
1896- svc := NewPostalService(ss.bus, ss.notifBus, nil)
1897- c.Check(svc.Start(), Equals, NotConfigured)
1898+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, nil)
1899+ c.Check(svc.Start(), Equals, ErrNotConfigured)
1900 }
1901
1902 func (ss *postalSuite) TestStartNoBus(c *C) {
1903- svc := NewPostalService(nil, ss.notifBus, ss.log)
1904- c.Check(svc.Start(), Equals, NotConfigured)
1905+ svc := NewPostalService(nil, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1906+ c.Check(svc.Start(), Equals, ErrNotConfigured)
1907 }
1908
1909 func (ss *postalSuite) TestTakeTheBustFail(c *C) {
1910 nEndp := testibus.NewMultiValuedTestingEndpoint(condition.Work(true), condition.Work(false), []interface{}{uint32(1), "hello"})
1911- svc := NewPostalService(ss.bus, nEndp, ss.log)
1912+ svc := NewPostalService(ss.bus, nEndp, ss.counterBus, ss.hapticBus, ss.log)
1913 _, err := svc.TakeTheBus()
1914 c.Check(err, NotNil)
1915 }
1916
1917 func (ss *postalSuite) TestTakeTheBustOk(c *C) {
1918 nEndp := testibus.NewMultiValuedTestingEndpoint(condition.Work(true), condition.Work(true), []interface{}{uint32(1), "hello"})
1919- svc := NewPostalService(ss.bus, nEndp, ss.log)
1920+ svc := NewPostalService(ss.bus, nEndp, ss.counterBus, ss.hapticBus, ss.log)
1921 _, err := svc.TakeTheBus()
1922 c.Check(err, IsNil)
1923 }
1924
1925 func (ss *postalSuite) TestStartFailsOnBusDialFailure(c *C) {
1926 bus := testibus.NewTestingEndpoint(condition.Work(false), nil)
1927- svc := NewPostalService(bus, ss.notifBus, ss.log)
1928+ svc := NewPostalService(bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1929 c.Check(svc.Start(), ErrorMatches, `.*(?i)cond said no.*`)
1930 svc.Stop()
1931 }
1932
1933 func (ss *postalSuite) TestStartGrabsName(c *C) {
1934- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
1935+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1936 c.Assert(svc.Start(), IsNil)
1937 callArgs := testibus.GetCallArgs(ss.bus)
1938 defer svc.Stop()
1939@@ -98,7 +103,7 @@
1940 }
1941
1942 func (ss *postalSuite) TestStopClosesBus(c *C) {
1943- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
1944+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1945 c.Assert(svc.Start(), IsNil)
1946 svc.Stop()
1947 callArgs := testibus.GetCallArgs(ss.bus)
1948@@ -110,31 +115,31 @@
1949 // Injection tests
1950
1951 func (ss *postalSuite) TestInjectWorks(c *C) {
1952- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
1953+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1954 svc.msgHandler = nil
1955- rvs, err := svc.inject("/hello", []interface{}{"world"}, nil)
1956+ rvs, err := svc.inject(aPackageOnBus, []interface{}{anAppId, "world"}, nil)
1957 c.Assert(err, IsNil)
1958 c.Check(rvs, IsNil)
1959- rvs, err = svc.inject("/hello", []interface{}{"there"}, nil)
1960+ rvs, err = svc.inject(aPackageOnBus, []interface{}{anAppId, "there"}, nil)
1961 c.Assert(err, IsNil)
1962 c.Check(rvs, IsNil)
1963 c.Assert(svc.mbox, HasLen, 1)
1964- c.Assert(svc.mbox["hello"], HasLen, 2)
1965- c.Check(svc.mbox["hello"][0], Equals, "world")
1966- c.Check(svc.mbox["hello"][1], Equals, "there")
1967+ c.Assert(svc.mbox[anAppId], HasLen, 2)
1968+ c.Check(svc.mbox[anAppId][0], Equals, "world")
1969+ c.Check(svc.mbox[anAppId][1], Equals, "there")
1970
1971 // and check it fired the right signal (twice)
1972 callArgs := testibus.GetCallArgs(ss.bus)
1973 c.Assert(callArgs, HasLen, 2)
1974 c.Check(callArgs[0].Member, Equals, "::Signal")
1975- c.Check(callArgs[0].Args, DeepEquals, []interface{}{"Notification", "/hello", []interface{}{"hello"}})
1976+ c.Check(callArgs[0].Args, DeepEquals, []interface{}{"Post", aPackageOnBus, []interface{}{anAppId}})
1977 c.Check(callArgs[1], DeepEquals, callArgs[0])
1978 }
1979
1980 func (ss *postalSuite) TestInjectFailsIfInjectFails(c *C) {
1981 bus := testibus.NewTestingEndpoint(condition.Work(true),
1982 condition.Work(false))
1983- svc := NewPostalService(bus, ss.notifBus, ss.log)
1984+ svc := NewPostalService(bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
1985 svc.SetMessageHandler(func(string, string, *launch_helper.HelperOutput) error { return errors.New("fail") })
1986 _, err := svc.inject("/hello", []interface{}{"xyzzy"}, nil)
1987 c.Check(err, NotNil)
1988@@ -145,22 +150,57 @@
1989 args []interface{}
1990 errt error
1991 }{
1992- {nil, BadArgCount},
1993- {[]interface{}{}, BadArgCount},
1994- {[]interface{}{1}, BadArgType},
1995- {[]interface{}{1, 2}, BadArgCount},
1996+ {nil, ErrBadArgCount},
1997+ {[]interface{}{}, ErrBadArgCount},
1998+ {[]interface{}{1}, ErrBadArgCount},
1999+ {[]interface{}{anAppId, 1}, ErrBadArgType},
2000+ {[]interface{}{1, "hello"}, ErrBadArgType},
2001+ {[]interface{}{1, 2, 3}, ErrBadArgCount},
2002+ {[]interface{}{"bar", "hello"}, ErrBadAppId},
2003 } {
2004- reg, err := new(PostalService).inject("", s.args, nil)
2005+ reg, err := new(PostalService).inject(aPackageOnBus, s.args, nil)
2006 c.Check(reg, IsNil, Commentf("iteration #%d", i))
2007 c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
2008 }
2009 }
2010
2011 //
2012+// Injection (Broadcast) tests
2013+
2014+func (ss *postalSuite) TestInjectBroadcast(c *C) {
2015+ bus := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
2016+ svc := NewPostalService(ss.bus, bus, ss.counterBus, ss.hapticBus, ss.log)
2017+ //svc.msgHandler = nil
2018+ rvs, err := svc.InjectBroadcast()
2019+ c.Assert(err, IsNil)
2020+ c.Check(rvs, Equals, uint32(0))
2021+ c.Assert(err, IsNil)
2022+ // and check it fired the right signal (twice)
2023+ callArgs := testibus.GetCallArgs(bus)
2024+ c.Assert(callArgs, HasLen, 1)
2025+ c.Check(callArgs[0].Member, Equals, "Notify")
2026+ c.Check(callArgs[0].Args[0:6], DeepEquals, []interface{}{"ubuntu-push-client", uint32(0), "update_manager_icon",
2027+ "There's an updated system image.", "Tap to open the system updater.",
2028+ []string{"ubuntu-push-client::settings:///system/system-update::0", "Switch to app"}})
2029+ // TODO: check the map in callArgs?
2030+ // c.Check(callArgs[0].Args[7]["x-canonical-secondary-icon"], NotNil)
2031+ // c.Check(callArgs[0].Args[7]["x-canonical-snap-decisions"], NotNil)
2032+}
2033+
2034+func (ss *postalSuite) TestInjectBroadcastFails(c *C) {
2035+ bus := testibus.NewTestingEndpoint(condition.Work(true),
2036+ condition.Work(false))
2037+ svc := NewPostalService(ss.bus, bus, ss.counterBus, ss.hapticBus, ss.log)
2038+ svc.SetMessageHandler(func(string, string, *launch_helper.HelperOutput) error { return errors.New("fail") })
2039+ _, err := svc.InjectBroadcast()
2040+ c.Check(err, NotNil)
2041+}
2042+
2043+//
2044 // Notifications tests
2045 func (ss *postalSuite) TestNotificationsWorks(c *C) {
2046- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
2047- nots, err := svc.notifications("/hello", nil, nil)
2048+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
2049+ nots, err := svc.notifications(aPackageOnBus, []interface{}{anAppId}, nil)
2050 c.Assert(err, IsNil)
2051 c.Assert(nots, NotNil)
2052 c.Assert(nots, HasLen, 1)
2053@@ -168,8 +208,8 @@
2054 if svc.mbox == nil {
2055 svc.mbox = make(map[string][]string)
2056 }
2057- svc.mbox["hello"] = append(svc.mbox["hello"], "this", "thing")
2058- nots, err = svc.notifications("/hello", nil, nil)
2059+ svc.mbox[anAppId] = append(svc.mbox[anAppId], "this", "thing")
2060+ nots, err = svc.notifications(aPackageOnBus, []interface{}{anAppId}, nil)
2061 c.Assert(err, IsNil)
2062 c.Assert(nots, NotNil)
2063 c.Assert(nots, HasLen, 1)
2064@@ -177,9 +217,19 @@
2065 }
2066
2067 func (ss *postalSuite) TestNotificationsFailsIfBadArgs(c *C) {
2068- reg, err := new(PostalService).notifications("/foo", []interface{}{1}, nil)
2069- c.Check(reg, IsNil)
2070- c.Check(err, Equals, BadArgCount)
2071+ for i, s := range []struct {
2072+ args []interface{}
2073+ errt error
2074+ }{
2075+ {nil, ErrBadArgCount},
2076+ {[]interface{}{}, ErrBadArgCount},
2077+ {[]interface{}{1}, ErrBadArgType},
2078+ {[]interface{}{"potato"}, ErrBadAppId},
2079+ } {
2080+ reg, err := new(PostalService).notifications(aPackageOnBus, s.args, nil)
2081+ c.Check(reg, IsNil, Commentf("iteration #%d", i))
2082+ c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
2083+ }
2084 }
2085
2086 func (ss *postalSuite) TestMessageHandlerPublicAPI(c *C) {
2087@@ -198,35 +248,44 @@
2088
2089 func (ss *postalSuite) TestInjectCallsMessageHandler(c *C) {
2090 var ext = &launch_helper.HelperOutput{}
2091- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
2092+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
2093 f := func(app string, nid string, s *launch_helper.HelperOutput) error { ext = s; return nil }
2094 svc.SetMessageHandler(f)
2095- c.Check(svc.Inject("stuff", "thing", "{}"), IsNil)
2096+ c.Check(svc.Inject("pkg", "app", "thing", "{}"), IsNil)
2097 c.Check(ext, DeepEquals, &launch_helper.HelperOutput{})
2098 err := errors.New("ouch")
2099 svc.SetMessageHandler(func(string, string, *launch_helper.HelperOutput) error { return err })
2100- c.Check(svc.Inject("stuff", "", "{}"), Equals, err)
2101+ c.Check(svc.Inject("pkg", "app", "", "{}"), Equals, err)
2102 }
2103
2104-func (ss *postalSuite) TestMessageHandler(c *C) {
2105+func (ss *postalSuite) TestMessageHandlerPresents(c *C) {
2106 endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
2107- svc := NewPostalService(ss.bus, endp, ss.log)
2108- card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Body: "body-value", Popup: true}
2109- output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: card}}
2110- err := svc.messageHandler("xyzzy", "", output)
2111+ svc := NewPostalService(endp, endp, endp, endp, ss.log)
2112+ // Persist is false so we just check the log
2113+ card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Body: "body-value", Popup: true, Persist: false}
2114+ vib := &launch_helper.Vibration{Duration: 500}
2115+ emb := &launch_helper.EmblemCounter{Count: 2, Visible: true}
2116+ output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: card, EmblemCounter: emb, Vibrate: vib}}
2117+ err := svc.messageHandler("com.example.test_test", "", output)
2118 c.Assert(err, IsNil)
2119 args := testibus.GetCallArgs(endp)
2120- c.Assert(args, HasLen, 1)
2121- c.Check(args[0].Member, Equals, "Notify")
2122- c.Check(args[0].Args[0], Equals, "xyzzy")
2123- c.Check(args[0].Args[2], Equals, "icon-value")
2124- c.Check(args[0].Args[3], Equals, "summary-value")
2125- c.Check(args[0].Args[4], Equals, "body-value")
2126+ c.Assert(args, HasLen, 4)
2127+ mm := make([]string, len(args))
2128+ for i, m := range args {
2129+ mm[i] = m.Member
2130+ }
2131+ sort.Strings(mm)
2132+ // check the Present() methods were called.
2133+ // For dbus-backed presenters, just check the right dbus methods are called
2134+ c.Check(mm, DeepEquals, []string{"::SetProperty", "::SetProperty", "Notify", "VibratePattern"})
2135+ // For the other ones, check the logs
2136+ c.Check(ss.log.Captured(), Matches, `(?sm).* no persistable card:.*`)
2137+ c.Check(ss.log.Captured(), Matches, `(?sm).* no Sound in the notification.*`)
2138 }
2139
2140 func (ss *postalSuite) TestMessageHandlerReportsFailedNotifies(c *C) {
2141 endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
2142- svc := NewPostalService(ss.bus, endp, ss.log)
2143+ svc := NewPostalService(ss.bus, endp, ss.counterBus, ss.hapticBus, ss.log)
2144 card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Body: "body-value", Popup: true}
2145 notif := &launch_helper.Notification{Card: card}
2146 output := &launch_helper.HelperOutput{Notification: notif}
2147@@ -235,7 +294,7 @@
2148 }
2149
2150 func (ss *postalSuite) TestMessageHandlerReportsButIgnoresUnmarshalErrors(c *C) {
2151- svc := NewPostalService(ss.bus, ss.notifBus, ss.log)
2152+ svc := NewPostalService(ss.bus, ss.notifBus, ss.counterBus, ss.hapticBus, ss.log)
2153 output := &launch_helper.HelperOutput{[]byte(`broken`), nil}
2154 err := svc.messageHandler("", "", output)
2155 c.Check(err, IsNil)
2156@@ -244,7 +303,7 @@
2157
2158 func (ss *postalSuite) TestMessageHandlerReportsButIgnoresNilNotifies(c *C) {
2159 endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
2160- svc := NewPostalService(ss.bus, endp, ss.log)
2161+ svc := NewPostalService(ss.bus, endp, ss.counterBus, ss.hapticBus, ss.log)
2162 output := &launch_helper.HelperOutput{[]byte(`{}`), nil}
2163 err := svc.messageHandler("", "", output)
2164 c.Assert(err, IsNil)
2165
2166=== modified file 'client/service/service.go'
2167--- client/service/service.go 2014-07-01 11:55:30 +0000
2168+++ client/service/service.go 2014-07-07 14:23:18 +0000
2169@@ -23,8 +23,8 @@
2170 "fmt"
2171 "io/ioutil"
2172 "net/http"
2173+ "net/url"
2174 "os"
2175- "strings"
2176
2177 "launchpad.net/ubuntu-push/bus"
2178 http13 "launchpad.net/ubuntu-push/http13client"
2179@@ -32,10 +32,17 @@
2180 "launchpad.net/ubuntu-push/nih"
2181 )
2182
2183+// PushServiceSetup encapsulates the params for setting up a PushService.
2184+type PushServiceSetup struct {
2185+ RegURL *url.URL
2186+ DeviceId string
2187+ AuthGetter func(string) string
2188+}
2189+
2190 // PushService is the dbus api
2191 type PushService struct {
2192 DBusService
2193- regURL string
2194+ regURL *url.URL
2195 deviceId string
2196 authGetter func(string) string
2197 httpCli http13.Client
2198@@ -50,72 +57,42 @@
2199 )
2200
2201 // NewPushService() builds a new service and returns it.
2202-func NewPushService(bus bus.Endpoint, log logger.Logger) *PushService {
2203+func NewPushService(bus bus.Endpoint, setup *PushServiceSetup, log logger.Logger) *PushService {
2204 var svc = &PushService{}
2205 svc.Log = log
2206 svc.Bus = bus
2207+ svc.regURL = setup.RegURL
2208+ svc.deviceId = setup.DeviceId
2209+ svc.authGetter = setup.AuthGetter
2210 return svc
2211 }
2212
2213-// SetRegistrationURL() sets the registration url for the service
2214-func (svc *PushService) SetRegistrationURL(url string) {
2215- svc.lock.Lock()
2216- defer svc.lock.Unlock()
2217- svc.regURL = url
2218-}
2219-
2220-// SetAuthGetter() sets the authorization getter for the service
2221-func (svc *PushService) SetAuthGetter(authGetter func(string) string) {
2222- svc.lock.Lock()
2223- defer svc.lock.Unlock()
2224- svc.authGetter = authGetter
2225-}
2226-
2227-// getRegistrationAuthorization() returns the authorization header for
2228-// POSTing to the registration HTTP endpoint
2229-//
2230-// (this is for calling with the lock held)
2231-func (svc *PushService) getRegistrationAuthorization() string {
2232- if svc.authGetter != nil && svc.regURL != "" {
2233- return svc.authGetter(svc.regURL)
2234- } else {
2235- return ""
2236- }
2237-}
2238-
2239-// GetRegistrationAuthorization() returns the authorization header for
2240-// POSTing to the registration HTTP endpoint
2241-func (svc *PushService) GetRegistrationAuthorization() string {
2242- svc.lock.RLock()
2243- defer svc.lock.RUnlock()
2244- return svc.getRegistrationAuthorization()
2245-}
2246-
2247-// SetDeviceId() sets the device id
2248-func (svc *PushService) SetDeviceId(deviceId string) {
2249- svc.lock.Lock()
2250- defer svc.lock.Unlock()
2251- svc.deviceId = deviceId
2252-}
2253-
2254-// GetDeviceId() returns the device id
2255-func (svc *PushService) GetDeviceId() string {
2256- svc.lock.RLock()
2257- defer svc.lock.RUnlock()
2258- return svc.deviceId
2259+// getAuthorization() returns the URL and the authorization header for
2260+// POSTing to the registration HTTP endpoint for op
2261+func (svc *PushService) getAuthorization(op string) (string, string) {
2262+ if svc.authGetter == nil || svc.regURL == nil {
2263+ return "", ""
2264+ }
2265+ purl, err := svc.regURL.Parse(op)
2266+ if err != nil {
2267+ panic("op to getAuthorization was invalid")
2268+ }
2269+ url := purl.String()
2270+ return url, svc.authGetter(url)
2271 }
2272
2273 func (svc *PushService) Start() error {
2274 return svc.DBusService.Start(bus.DispatchMap{
2275- "Register": svc.register,
2276+ "Register": svc.register,
2277+ "Unregister": svc.unregister,
2278 }, PushServiceBusAddress)
2279 }
2280
2281 var (
2282- BadServer = errors.New("Bad server")
2283- BadRequest = errors.New("Bad request")
2284- BadToken = errors.New("Bad token")
2285- BadAuth = errors.New("Bad auth")
2286+ ErrBadServer = errors.New("bad server")
2287+ ErrBadRequest = errors.New("bad request")
2288+ ErrBadToken = errors.New("bad token")
2289+ ErrBadAuth = errors.New("bad auth")
2290 )
2291
2292 type registrationRequest struct {
2293@@ -130,31 +107,20 @@
2294 Message string `json:"message"` //
2295 }
2296
2297-func (svc *PushService) register(path string, args, _ []interface{}) ([]interface{}, error) {
2298- svc.lock.RLock()
2299- defer svc.lock.RUnlock()
2300- if len(args) != 0 {
2301- return nil, BadArgCount
2302- }
2303- raw_appname := path[strings.LastIndex(path, "/")+1:]
2304- appname := string(nih.Unquote([]byte(raw_appname)))
2305-
2306- rv := os.Getenv("PUSH_REG_" + raw_appname)
2307- if rv != "" {
2308- return []interface{}{rv}, nil
2309- }
2310-
2311- req_body, err := json.Marshal(registrationRequest{svc.deviceId, appname})
2312+func (svc *PushService) manageReg(op, appId string) (*registrationReply, error) {
2313+ req_body, err := json.Marshal(registrationRequest{svc.deviceId, appId})
2314 if err != nil {
2315 return nil, fmt.Errorf("unable to marshal register request body: %v", err)
2316 }
2317- req, err := http13.NewRequest("POST", svc.regURL, bytes.NewReader(req_body))
2318- if err != nil {
2319- return nil, fmt.Errorf("unable to build register request: %v", err)
2320- }
2321- auth := svc.getRegistrationAuthorization()
2322+
2323+ url, auth := svc.getAuthorization(op)
2324 if auth == "" {
2325- return nil, BadAuth
2326+ return nil, ErrBadAuth
2327+ }
2328+
2329+ req, err := http13.NewRequest("POST", url, bytes.NewReader(req_body))
2330+ if err != nil {
2331+ panic(fmt.Errorf("unable to build register request: %v", err))
2332 }
2333 req.Header.Add("Authorization", auth)
2334 req.Header.Add("Content-Type", "application/json")
2335@@ -169,9 +135,9 @@
2336 switch {
2337 case resp.StatusCode >= http.StatusInternalServerError:
2338 // XXX retry on 503
2339- return nil, BadServer
2340+ return nil, ErrBadServer
2341 default:
2342- return nil, BadRequest
2343+ return nil, ErrBadRequest
2344 }
2345 }
2346 // errors below here Can't Happen (tm).
2347@@ -188,10 +154,44 @@
2348 return nil, fmt.Errorf("unable to unmarshal register response: %v", err)
2349 }
2350
2351+ return &reply, nil
2352+}
2353+
2354+func (svc *PushService) register(path string, args, _ []interface{}) ([]interface{}, error) {
2355+ _, appId, err := grabDBusPackageAndAppId(path, args, 0)
2356+ if err != nil {
2357+ return nil, err
2358+ }
2359+
2360+ rawAppId := string(nih.Quote([]byte(appId)))
2361+ rv := os.Getenv("PUSH_REG_" + rawAppId)
2362+ if rv != "" {
2363+ return []interface{}{rv}, nil
2364+ }
2365+
2366+ reply, err := svc.manageReg("/register", appId)
2367+ if err != nil {
2368+ return nil, err
2369+ }
2370+
2371 if !reply.Ok || reply.Token == "" {
2372 svc.Log.Errorf("Unexpected response: %#v", reply)
2373- return nil, BadToken
2374+ return nil, ErrBadToken
2375 }
2376
2377 return []interface{}{reply.Token}, nil
2378 }
2379+
2380+func (svc *PushService) unregister(path string, args, _ []interface{}) ([]interface{}, error) {
2381+ _, appId, err := grabDBusPackageAndAppId(path, args, 0)
2382+ if err != nil {
2383+ return nil, err
2384+ }
2385+
2386+ return nil, svc.Unregister(appId)
2387+}
2388+
2389+func (svc *PushService) Unregister(appId string) error {
2390+ _, err := svc.manageReg("/unregister", appId)
2391+ return err
2392+}
2393
2394=== modified file 'client/service/service_test.go'
2395--- client/service/service_test.go 2014-06-20 12:33:03 +0000
2396+++ client/service/service_test.go 2014-07-07 14:23:18 +0000
2397@@ -29,6 +29,7 @@
2398 "launchpad.net/ubuntu-push/bus"
2399 testibus "launchpad.net/ubuntu-push/bus/testing"
2400 "launchpad.net/ubuntu-push/logger"
2401+ "launchpad.net/ubuntu-push/nih"
2402 helpers "launchpad.net/ubuntu-push/testing"
2403 "launchpad.net/ubuntu-push/testing/condition"
2404 )
2405@@ -42,13 +43,36 @@
2406
2407 var _ = Suite(&serviceSuite{})
2408
2409+var (
2410+ aPackage = "com.example.test"
2411+ anAppId = aPackage + "_test-number-one"
2412+ aPackageOnBus = "/" + string(nih.Quote([]byte(aPackage)))
2413+)
2414+
2415 func (ss *serviceSuite) SetUpTest(c *C) {
2416 ss.log = helpers.NewTestLogger(c, "debug")
2417 ss.bus = testibus.NewTestingEndpoint(condition.Work(true), nil)
2418 }
2419
2420+var testSetup = &PushServiceSetup{}
2421+
2422+func (ss *serviceSuite) TestBuild(c *C) {
2423+ setup := &PushServiceSetup{
2424+ RegURL: helpers.ParseURL("http://reg"),
2425+ DeviceId: "FOO",
2426+ AuthGetter: func(s string) string {
2427+ return ""
2428+ },
2429+ }
2430+ svc := NewPushService(ss.bus, setup, ss.log)
2431+ c.Check(svc.Bus, Equals, ss.bus)
2432+ c.Check(svc.regURL, DeepEquals, helpers.ParseURL("http://reg"))
2433+ c.Check(fmt.Sprintf("%#v", svc.authGetter), Equals, fmt.Sprintf("%#v", setup.AuthGetter))
2434+ // ...
2435+}
2436+
2437 func (ss *serviceSuite) TestStart(c *C) {
2438- svc := NewPushService(ss.bus, ss.log)
2439+ svc := NewPushService(ss.bus, testSetup, ss.log)
2440 c.Check(svc.IsRunning(), Equals, false)
2441 c.Check(svc.Start(), IsNil)
2442 c.Check(svc.IsRunning(), Equals, true)
2443@@ -56,31 +80,31 @@
2444 }
2445
2446 func (ss *serviceSuite) TestStartTwice(c *C) {
2447- svc := NewPushService(ss.bus, ss.log)
2448+ svc := NewPushService(ss.bus, testSetup, ss.log)
2449 c.Check(svc.Start(), IsNil)
2450- c.Check(svc.Start(), Equals, AlreadyStarted)
2451+ c.Check(svc.Start(), Equals, ErrAlreadyStarted)
2452 svc.Stop()
2453 }
2454
2455 func (ss *serviceSuite) TestStartNoLog(c *C) {
2456- svc := NewPushService(ss.bus, nil)
2457- c.Check(svc.Start(), Equals, NotConfigured)
2458+ svc := NewPushService(ss.bus, testSetup, nil)
2459+ c.Check(svc.Start(), Equals, ErrNotConfigured)
2460 }
2461
2462 func (ss *serviceSuite) TestStartNoBus(c *C) {
2463- svc := NewPushService(nil, ss.log)
2464- c.Check(svc.Start(), Equals, NotConfigured)
2465+ svc := NewPushService(nil, testSetup, ss.log)
2466+ c.Check(svc.Start(), Equals, ErrNotConfigured)
2467 }
2468
2469 func (ss *serviceSuite) TestStartFailsOnBusDialFailure(c *C) {
2470 bus := testibus.NewTestingEndpoint(condition.Work(false), nil)
2471- svc := NewPushService(bus, ss.log)
2472+ svc := NewPushService(bus, testSetup, ss.log)
2473 c.Check(svc.Start(), ErrorMatches, `.*(?i)cond said no.*`)
2474 svc.Stop()
2475 }
2476
2477 func (ss *serviceSuite) TestStartGrabsName(c *C) {
2478- svc := NewPushService(ss.bus, ss.log)
2479+ svc := NewPushService(ss.bus, testSetup, ss.log)
2480 c.Assert(svc.Start(), IsNil)
2481 callArgs := testibus.GetCallArgs(ss.bus)
2482 defer svc.Stop()
2483@@ -89,7 +113,7 @@
2484 }
2485
2486 func (ss *serviceSuite) TestStopClosesBus(c *C) {
2487- svc := NewPushService(ss.bus, ss.log)
2488+ svc := NewPushService(ss.bus, testSetup, ss.log)
2489 c.Assert(svc.Start(), IsNil)
2490 svc.Stop()
2491 callArgs := testibus.GetCallArgs(ss.bus)
2492@@ -99,41 +123,48 @@
2493
2494 // registration tests
2495
2496-func (ss *serviceSuite) TestSetRegURLWorks(c *C) {
2497- svc := NewPushService(ss.bus, ss.log)
2498- c.Check(svc.regURL, Equals, "")
2499- svc.SetRegistrationURL("xyzzy://")
2500- c.Check(svc.regURL, Equals, "xyzzy://")
2501-}
2502-
2503-func (ss *serviceSuite) TestSetAuthGetterWorks(c *C) {
2504- svc := NewPushService(ss.bus, ss.log)
2505- c.Check(svc.authGetter, IsNil)
2506- f := func(string) string { return "" }
2507- svc.SetAuthGetter(f)
2508- c.Check(fmt.Sprintf("%#v", svc.authGetter), Equals, fmt.Sprintf("%#v", f))
2509-}
2510-
2511 func (ss *serviceSuite) TestGetRegAuthWorks(c *C) {
2512- svc := NewPushService(ss.bus, ss.log)
2513- svc.SetRegistrationURL("xyzzy://")
2514 ch := make(chan string, 1)
2515- f := func(s string) string { ch <- s; return "Auth " + s }
2516- svc.SetAuthGetter(f)
2517- c.Check(svc.getRegistrationAuthorization(), Equals, "Auth xyzzy://")
2518+ setup := &PushServiceSetup{
2519+ RegURL: helpers.ParseURL("http://foo"),
2520+ AuthGetter: func(s string) string {
2521+ ch <- s
2522+ return "Auth " + s
2523+ },
2524+ }
2525+ svc := NewPushService(ss.bus, setup, ss.log)
2526+ url, auth := svc.getAuthorization("/op")
2527+ c.Check(auth, Equals, "Auth http://foo/op")
2528 c.Assert(len(ch), Equals, 1)
2529- c.Check(<-ch, Equals, "xyzzy://")
2530+ c.Check(<-ch, Equals, "http://foo/op")
2531+ c.Check(url, Equals, "http://foo/op")
2532 }
2533
2534 func (ss *serviceSuite) TestGetRegAuthDoesNotPanic(c *C) {
2535- svc := NewPushService(ss.bus, ss.log)
2536- c.Check(svc.getRegistrationAuthorization(), Equals, "")
2537+ svc := NewPushService(ss.bus, testSetup, ss.log)
2538+ _, auth := svc.getAuthorization("/op")
2539+ c.Check(auth, Equals, "")
2540 }
2541
2542-func (ss *serviceSuite) TestRegistrationFailsIfBadArgs(c *C) {
2543- reg, err := new(PushService).register("", []interface{}{1}, nil)
2544- c.Check(reg, IsNil)
2545- c.Check(err, Equals, BadArgCount)
2546+func (ss *serviceSuite) TestRegistrationAndUnregistrationFailIfBadArgs(c *C) {
2547+ for i, s := range []struct {
2548+ args []interface{}
2549+ errt error
2550+ }{
2551+ {nil, ErrBadArgCount},
2552+ {[]interface{}{}, ErrBadArgCount},
2553+ {[]interface{}{1}, ErrBadArgType},
2554+ {[]interface{}{"foo"}, ErrBadAppId},
2555+ {[]interface{}{"foo", "bar"}, ErrBadArgCount},
2556+ } {
2557+ reg, err := new(PushService).register("/bar", s.args, nil)
2558+ c.Check(reg, IsNil, Commentf("iteration #%d", i))
2559+ c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
2560+
2561+ reg, err = new(PushService).unregister("/bar", s.args, nil)
2562+ c.Check(reg, IsNil, Commentf("iteration #%d", i))
2563+ c.Check(err, Equals, s.errt, Commentf("iteration #%d", i))
2564+ }
2565 }
2566
2567 func (ss *serviceSuite) TestRegistrationWorks(c *C) {
2568@@ -143,19 +174,20 @@
2569 c.Assert(e, IsNil)
2570 req := registrationRequest{}
2571 c.Assert(json.Unmarshal(buf[:n], &req), IsNil)
2572- c.Check(req, DeepEquals, registrationRequest{"fake-device-id", "an-app-id"})
2573-
2574+ c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId})
2575+ c.Check(r.URL.Path, Equals, "/register")
2576 w.Header().Set("Content-Type", "application/json")
2577 fmt.Fprintln(w, `{"ok":true,"token":"blob-of-bytes"}`)
2578 }))
2579 defer ts.Close()
2580-
2581- svc := NewPushService(ss.bus, ss.log)
2582- svc.SetAuthGetter(func(string) string { return "tok" })
2583- svc.SetRegistrationURL(ts.URL)
2584- svc.SetDeviceId("fake-device-id")
2585+ setup := &PushServiceSetup{
2586+ DeviceId: "fake-device-id",
2587+ RegURL: helpers.ParseURL(ts.URL),
2588+ AuthGetter: func(string) string { return "tok" },
2589+ }
2590+ svc := NewPushService(ss.bus, setup, ss.log)
2591 // this'll check (un)quoting, too
2592- reg, err := svc.register("/an_2dapp_2did", nil, nil)
2593+ reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil)
2594 c.Assert(err, IsNil)
2595 c.Assert(reg, HasLen, 1)
2596 regs, ok := reg[0].(string)
2597@@ -164,10 +196,11 @@
2598 }
2599
2600 func (ss *serviceSuite) TestRegistrationOverrideWorks(c *C) {
2601- os.Setenv("PUSH_REG_stuff", "42")
2602- defer os.Setenv("PUSH_REG_stuff", "")
2603+ envar := "PUSH_REG_" + string(nih.Quote([]byte(anAppId)))
2604+ os.Setenv(envar, "42")
2605+ defer os.Setenv(envar, "")
2606
2607- reg, err := new(PushService).register("/stuff", nil, nil)
2608+ reg, err := new(PushService).register(aPackageOnBus, []interface{}{anAppId}, nil)
2609 c.Assert(reg, HasLen, 1)
2610 regs, ok := reg[0].(string)
2611 c.Check(ok, Equals, true)
2612@@ -175,103 +208,155 @@
2613 c.Check(err, IsNil)
2614 }
2615
2616-func (ss *serviceSuite) TestRegistrationFailsOnBadReqURL(c *C) {
2617- svc := NewPushService(ss.bus, ss.log)
2618- svc.SetRegistrationURL("%gh")
2619- reg, err := svc.register("thing", nil, nil)
2620- c.Check(reg, IsNil)
2621- c.Check(err, ErrorMatches, "unable to build register request: .*")
2622-}
2623-
2624-func (ss *serviceSuite) TestRegistrationFailsOnBadAuth(c *C) {
2625- svc := NewPushService(ss.bus, ss.log)
2626+func (ss *serviceSuite) TestManageRegFailsOnBadAuth(c *C) {
2627 // ... no auth added
2628- reg, err := svc.register("thing", nil, nil)
2629+ svc := NewPushService(ss.bus, testSetup, ss.log)
2630+ reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil)
2631 c.Check(reg, IsNil)
2632- c.Check(err, Equals, BadAuth)
2633+ c.Check(err, Equals, ErrBadAuth)
2634 }
2635
2636-func (ss *serviceSuite) TestRegistrationFailsOnNoServer(c *C) {
2637- svc := NewPushService(ss.bus, ss.log)
2638- svc.SetRegistrationURL("xyzzy://")
2639- svc.SetAuthGetter(func(string) string { return "tok" })
2640- reg, err := svc.register("thing", nil, nil)
2641+func (ss *serviceSuite) TestManageRegFailsOnNoServer(c *C) {
2642+ setup := &PushServiceSetup{
2643+ DeviceId: "fake-device-id",
2644+ RegURL: helpers.ParseURL("xyzzy://"),
2645+ AuthGetter: func(string) string { return "tok" },
2646+ }
2647+ svc := NewPushService(ss.bus, setup, ss.log)
2648+ reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil)
2649 c.Check(reg, IsNil)
2650 c.Check(err, ErrorMatches, "unable to request registration: .*")
2651 }
2652
2653-func (ss *serviceSuite) TestRegistrationFailsOn40x(c *C) {
2654+func (ss *serviceSuite) TestManageRegFailsOn40x(c *C) {
2655 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2656 http.Error(w, "I'm a teapot", 418)
2657 }))
2658 defer ts.Close()
2659-
2660- svc := NewPushService(ss.bus, ss.log)
2661- svc.SetAuthGetter(func(string) string { return "tok" })
2662- svc.SetRegistrationURL(ts.URL)
2663- reg, err := svc.register("/thing", nil, nil)
2664- c.Check(err, Equals, BadRequest)
2665+ setup := &PushServiceSetup{
2666+ DeviceId: "fake-device-id",
2667+ RegURL: helpers.ParseURL(ts.URL),
2668+ AuthGetter: func(string) string { return "tok" },
2669+ }
2670+ svc := NewPushService(ss.bus, setup, ss.log)
2671+ reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil)
2672+ c.Check(err, Equals, ErrBadRequest)
2673 c.Check(reg, IsNil)
2674 }
2675
2676-func (ss *serviceSuite) TestRegistrationFailsOn50x(c *C) {
2677+func (ss *serviceSuite) TestManageRegFailsOn50x(c *C) {
2678 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2679 http.Error(w, "Not implemented", 501)
2680 }))
2681 defer ts.Close()
2682-
2683- svc := NewPushService(ss.bus, ss.log)
2684- svc.SetAuthGetter(func(string) string { return "tok" })
2685- svc.SetRegistrationURL(ts.URL)
2686- reg, err := svc.register("/thing", nil, nil)
2687- c.Check(err, Equals, BadServer)
2688+ setup := &PushServiceSetup{
2689+ DeviceId: "fake-device-id",
2690+ RegURL: helpers.ParseURL(ts.URL),
2691+ AuthGetter: func(string) string { return "tok" },
2692+ }
2693+ svc := NewPushService(ss.bus, setup, ss.log)
2694+ reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil)
2695+ c.Check(err, Equals, ErrBadServer)
2696 c.Check(reg, IsNil)
2697 }
2698
2699-func (ss *serviceSuite) TestRegistrationFailsOnBadJSON(c *C) {
2700+func (ss *serviceSuite) TestManageRegFailsOnBadJSON(c *C) {
2701 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2702 buf := make([]byte, 256)
2703 n, e := r.Body.Read(buf)
2704 c.Assert(e, IsNil)
2705 req := registrationRequest{}
2706 c.Assert(json.Unmarshal(buf[:n], &req), IsNil)
2707- c.Check(req, DeepEquals, registrationRequest{"fake-device-id", "an-app-id"})
2708+ c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId})
2709
2710 w.Header().Set("Content-Type", "application/json")
2711 fmt.Fprintln(w, `{`)
2712 }))
2713 defer ts.Close()
2714-
2715- svc := NewPushService(ss.bus, ss.log)
2716- svc.SetAuthGetter(func(string) string { return "tok" })
2717- svc.SetRegistrationURL(ts.URL)
2718- svc.SetDeviceId("fake-device-id")
2719+ setup := &PushServiceSetup{
2720+ DeviceId: "fake-device-id",
2721+ RegURL: helpers.ParseURL(ts.URL),
2722+ AuthGetter: func(string) string { return "tok" },
2723+ }
2724+ svc := NewPushService(ss.bus, setup, ss.log)
2725 // this'll check (un)quoting, too
2726- reg, err := svc.register("/an_2dapp_2did", nil, nil)
2727+ reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil)
2728 c.Check(reg, IsNil)
2729 c.Check(err, ErrorMatches, "unable to unmarshal register response: .*")
2730 }
2731
2732-func (ss *serviceSuite) TestRegistrationFailsOnBadJSONDocument(c *C) {
2733+func (ss *serviceSuite) TestManageRegFailsOnBadJSONDocument(c *C) {
2734 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2735 buf := make([]byte, 256)
2736 n, e := r.Body.Read(buf)
2737 c.Assert(e, IsNil)
2738 req := registrationRequest{}
2739 c.Assert(json.Unmarshal(buf[:n], &req), IsNil)
2740- c.Check(req, DeepEquals, registrationRequest{"fake-device-id", "an-app-id"})
2741+ c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId})
2742
2743 w.Header().Set("Content-Type", "application/json")
2744 fmt.Fprintln(w, `{"bananas": "very yes"}`)
2745 }))
2746 defer ts.Close()
2747-
2748- svc := NewPushService(ss.bus, ss.log)
2749- svc.SetAuthGetter(func(string) string { return "tok" })
2750- svc.SetRegistrationURL(ts.URL)
2751- svc.SetDeviceId("fake-device-id")
2752+ setup := &PushServiceSetup{
2753+ DeviceId: "fake-device-id",
2754+ RegURL: helpers.ParseURL(ts.URL),
2755+ AuthGetter: func(string) string { return "tok" },
2756+ }
2757+ svc := NewPushService(ss.bus, setup, ss.log)
2758 // this'll check (un)quoting, too
2759- reg, err := svc.register("/an_2dapp_2did", nil, nil)
2760+ reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil)
2761 c.Check(reg, IsNil)
2762- c.Check(err, Equals, BadToken)
2763+ c.Check(err, Equals, ErrBadToken)
2764+}
2765+
2766+func (ss *serviceSuite) TestDBusUnregisterWorks(c *C) {
2767+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2768+ buf := make([]byte, 256)
2769+ n, e := r.Body.Read(buf)
2770+ c.Assert(e, IsNil)
2771+ req := registrationRequest{}
2772+ c.Assert(json.Unmarshal(buf[:n], &req), IsNil)
2773+ c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId})
2774+ c.Check(r.URL.Path, Equals, "/unregister")
2775+ w.Header().Set("Content-Type", "application/json")
2776+ fmt.Fprintln(w, `{"ok":true,"token":"blob-of-bytes"}`)
2777+ }))
2778+ defer ts.Close()
2779+ setup := &PushServiceSetup{
2780+ DeviceId: "fake-device-id",
2781+ RegURL: helpers.ParseURL(ts.URL),
2782+ AuthGetter: func(string) string { return "tok" },
2783+ }
2784+ svc := NewPushService(ss.bus, setup, ss.log)
2785+ // this'll check (un)quoting, too
2786+ reg, err := svc.unregister(aPackageOnBus, []interface{}{anAppId}, nil)
2787+ c.Assert(err, IsNil)
2788+ c.Assert(reg, HasLen, 0)
2789+}
2790+
2791+func (ss *serviceSuite) TestUnregistrationWorks(c *C) {
2792+ invoked := make(chan bool, 1)
2793+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2794+ buf := make([]byte, 256)
2795+ n, e := r.Body.Read(buf)
2796+ c.Assert(e, IsNil)
2797+ req := registrationRequest{}
2798+ c.Assert(json.Unmarshal(buf[:n], &req), IsNil)
2799+ c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId})
2800+ c.Check(r.URL.Path, Equals, "/unregister")
2801+ invoked <- true
2802+ w.Header().Set("Content-Type", "application/json")
2803+ fmt.Fprintln(w, `{"ok":true}`)
2804+ }))
2805+ defer ts.Close()
2806+ setup := &PushServiceSetup{
2807+ DeviceId: "fake-device-id",
2808+ RegURL: helpers.ParseURL(ts.URL),
2809+ AuthGetter: func(string) string { return "tok" },
2810+ }
2811+ svc := NewPushService(ss.bus, setup, ss.log)
2812+ err := svc.Unregister(anAppId)
2813+ c.Assert(err, IsNil)
2814+ c.Check(invoked, HasLen, 1)
2815 }
2816
2817=== modified file 'client/session/session.go'
2818--- client/session/session.go 2014-06-13 12:19:26 +0000
2819+++ client/session/session.go 2014-07-07 14:23:18 +0000
2820@@ -78,6 +78,12 @@
2821 Get() (*gethosts.Host, error)
2822 }
2823
2824+// AddresseeChecking can check if a notification can be delivered.
2825+type AddresseeChecking interface {
2826+ StartAddresseeBatch()
2827+ CheckForAddressee(*protocol.Notification) bool
2828+}
2829+
2830 // ClientSessionConfig groups the client session configuration.
2831 type ClientSessionConfig struct {
2832 ConnectTimeout time.Duration
2833@@ -88,6 +94,7 @@
2834 Info map[string]interface{}
2835 AuthGetter func(string) string
2836 AuthURL string
2837+ AddresseeChecker AddresseeChecking
2838 }
2839
2840 // ClientSession holds a client<->server session and its configuration.
2841@@ -426,8 +433,12 @@
2842 return err
2843 }
2844 sess.clearShouldDelay()
2845+ sess.AddresseeChecker.StartAddresseeBatch()
2846 for i := range notifs {
2847 notif := &notifs[i]
2848+ if !sess.AddresseeChecker.CheckForAddressee(notif) {
2849+ continue
2850+ }
2851 sess.Log.Debugf("unicast app:%v msg:%s payload:%s",
2852 notif.AppId, notif.MsgId, notif.Payload)
2853 sess.Log.Debugf("sending ucast over")
2854
2855=== modified file 'client/session/session_test.go'
2856--- client/session/session_test.go 2014-06-13 12:19:26 +0000
2857+++ client/session/session_test.go 2014-07-07 14:23:18 +0000
2858@@ -798,7 +798,23 @@
2859 handleNotifications() tests
2860 ****************************************************************/
2861
2862+type testAddresseeChecking struct {
2863+ ops chan string
2864+ missing string
2865+}
2866+
2867+func (ac *testAddresseeChecking) StartAddresseeBatch() {
2868+ ac.ops <- "start"
2869+}
2870+
2871+func (ac *testAddresseeChecking) CheckForAddressee(notif *protocol.Notification) bool {
2872+ ac.ops <- notif.AppId
2873+ return notif.AppId != ac.missing
2874+}
2875+
2876 func (s *msgSuite) TestHandleNotificationsWorks(c *C) {
2877+ ac := &testAddresseeChecking{ops: make(chan string, 10)}
2878+ s.sess.AddresseeChecker = ac
2879 s.sess.setShouldDelay()
2880 n1 := protocol.Notification{
2881 AppId: "app1",
2882@@ -820,12 +836,52 @@
2883 s.upCh <- nil // ack ok
2884 c.Check(<-s.errCh, Equals, nil)
2885 c.Check(s.sess.ShouldDelay(), Equals, false)
2886- c.Assert(len(s.sess.NotificationsCh), Equals, 2)
2887+ c.Assert(s.sess.NotificationsCh, HasLen, 2)
2888 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n1)
2889 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n2)
2890+ c.Check(ac.ops, HasLen, 3)
2891+ c.Check(<-ac.ops, Equals, "start")
2892+ c.Check(<-ac.ops, Equals, "app1")
2893+ c.Check(<-ac.ops, Equals, "app2")
2894+}
2895+
2896+func (s *msgSuite) TestHandleNotificationsAddresseeCheck(c *C) {
2897+ ac := &testAddresseeChecking{
2898+ ops: make(chan string, 10),
2899+ missing: "app1",
2900+ }
2901+ s.sess.AddresseeChecker = ac
2902+ s.sess.setShouldDelay()
2903+ n1 := protocol.Notification{
2904+ AppId: "app1",
2905+ MsgId: "a",
2906+ Payload: json.RawMessage(`{"m": 1}`),
2907+ }
2908+ n2 := protocol.Notification{
2909+ AppId: "app2",
2910+ MsgId: "b",
2911+ Payload: json.RawMessage(`{"m": 2}`),
2912+ }
2913+ msg := serverMsg{"notifications",
2914+ protocol.BroadcastMsg{},
2915+ protocol.NotificationsMsg{
2916+ Notifications: []protocol.Notification{n1, n2},
2917+ }, protocol.ConnBrokenMsg{}}
2918+ go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
2919+ c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2920+ s.upCh <- nil // ack ok
2921+ c.Check(<-s.errCh, Equals, nil)
2922+ c.Check(s.sess.ShouldDelay(), Equals, false)
2923+ c.Assert(s.sess.NotificationsCh, HasLen, 1)
2924+ c.Check(<-s.sess.NotificationsCh, DeepEquals, &n2)
2925+ c.Check(ac.ops, HasLen, 3)
2926+ c.Check(<-ac.ops, Equals, "start")
2927+ c.Check(<-ac.ops, Equals, "app1")
2928 }
2929
2930 func (s *msgSuite) TestHandleNotificationsFiltersSeen(c *C) {
2931+ ac := &testAddresseeChecking{ops: make(chan string, 10)}
2932+ s.sess.AddresseeChecker = ac
2933 n1 := protocol.Notification{
2934 AppId: "app1",
2935 MsgId: "a",
2936@@ -845,16 +901,18 @@
2937 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2938 s.upCh <- nil // ack ok
2939 c.Check(<-s.errCh, Equals, nil)
2940- c.Assert(len(s.sess.NotificationsCh), Equals, 2)
2941+ c.Assert(s.sess.NotificationsCh, HasLen, 2)
2942 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n1)
2943 c.Check(<-s.sess.NotificationsCh, DeepEquals, &n2)
2944+ c.Check(ac.ops, HasLen, 3)
2945
2946 // second time they get ignored
2947 go func() { s.errCh <- s.sess.handleNotifications(&msg) }()
2948 c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
2949 s.upCh <- nil // ack ok
2950 c.Check(<-s.errCh, Equals, nil)
2951- c.Assert(len(s.sess.NotificationsCh), Equals, 0)
2952+ c.Assert(s.sess.NotificationsCh, HasLen, 0)
2953+ c.Check(ac.ops, HasLen, 4)
2954 }
2955
2956 func (s *msgSuite) TestHandleNotificationsBadAckWrite(c *C) {
2957
2958=== modified file 'debian/changelog'
2959--- debian/changelog 2014-07-02 13:14:04 +0000
2960+++ debian/changelog 2014-07-07 14:23:18 +0000
2961@@ -1,3 +1,18 @@
2962+ubuntu-push (0.43) UNRELEASED; urgency=medium
2963+
2964+ [ Samuele Pedroni ]
2965+ * Logic to support unregistering tokens lazily for uninstalled apps
2966+ * Minimal wrapping of libclick to check if a package is installed for a user
2967+ * Refactor and cleanup of cleanup/service
2968+
2969+ [ John R. Lenton ]
2970+ * Finalized DBus API (hopefully)
2971+ * Support emblem counter notifications
2972+ * Support haptic (vibration) notifications
2973+ * Support sound notifications
2974+
2975+ -- John R. Lenton <john.lenton@canonical.com> Mon, 07 Jul 2014 15:22:42 +0100
2976+
2977 ubuntu-push (0.42+14.10.20140702-0ubuntu1) utopic; urgency=medium
2978
2979 [ Samuele Pedroni ]
2980
2981=== modified file 'debian/config.json'
2982--- debian/config.json 2014-06-19 21:12:04 +0000
2983+++ debian/config.json 2014-07-07 14:23:18 +0000
2984@@ -1,7 +1,7 @@
2985 {
2986 "auth_helper": "/usr/lib/ubuntu-push-client/signing-helper",
2987 "session_url": "https://push.ubuntu.com/",
2988- "registration_url": "https://push.ubuntu.com/register",
2989+ "registration_url": "https://push.ubuntu.com",
2990 "connect_timeout": "20s",
2991 "exchange_timeout": "30s",
2992 "hosts_cache_expiry": "12h",
2993
2994=== modified file 'debian/control'
2995--- debian/control 2014-06-23 13:29:39 +0000
2996+++ debian/control 2014-07-07 14:23:18 +0000
2997@@ -20,6 +20,7 @@
2998 libubuntuoneauth-2.0-dev,
2999 libdbus-1-dev,
3000 libnih-dbus-dev,
3001+ libclick-0.4-dev,
3002 cmake,
3003 Standards-Version: 3.9.5
3004 Homepage: http://launchpad.net/ubuntu-push
3005
3006=== modified file 'launch_helper/helper_output.go'
3007--- launch_helper/helper_output.go 2014-06-30 12:19:14 +0000
3008+++ launch_helper/helper_output.go 2014-07-07 14:23:18 +0000
3009@@ -37,9 +37,9 @@
3010 // a Vibration generates a vibration in the form of a Pattern set in
3011 // duration a pattern of on off states, repeated a number of times
3012 type Vibration struct {
3013- Duration uint `json:"duration"` // if Duration is present and not 0, it's like a Pattern of [Duration] and a Repeat of 1; otherwise, Pattern and Repeat are used.
3014+ Duration uint32 `json:"duration"` // if Duration is present and not 0, it's like a Pattern of [Duration]; otherwise, Pattern is used.
3015 Pattern []uint32 `json:"pattern"`
3016- Repeat uint32 `json:"repeat"`
3017+ Repeat uint32 `json:"repeat"` // defaults to 1. A value of zero is ignored (so it's like 1).
3018 }
3019
3020 // a Notification can be any of the above
3021
3022=== modified file 'messaging/messaging.go'
3023--- messaging/messaging.go 2014-07-01 00:51:59 +0000
3024+++ messaging/messaging.go 2014-07-07 14:23:18 +0000
3025@@ -43,6 +43,7 @@
3026
3027 func (mmu *MessagingMenu) Present(appId string, notificationId string, notification *launch_helper.Notification) {
3028 if notification == nil || notification.Card == nil || !notification.Card.Persist || notification.Card.Summary == "" {
3029+ mmu.Log.Debugf("[%s] no notification or notification has no persistable card: %#v", notificationId, notification)
3030 return
3031 }
3032
3033
3034=== modified file 'messaging/messaging_test.go'
3035--- messaging/messaging_test.go 2014-07-01 00:51:59 +0000
3036+++ messaging/messaging_test.go 2014-07-07 14:23:18 +0000
3037@@ -61,7 +61,7 @@
3038
3039 mmu.Present("app-id", "notif-id", &notif)
3040
3041- c.Check(ms.log.Captured(), Equals, "")
3042+ c.Check(ms.log.Captured(), Matches, "(?sm).*has no persistable card.*")
3043 }
3044
3045 func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNotPersist(c *C) {
3046@@ -71,17 +71,17 @@
3047
3048 mmu.Present("app-id", "notif-id", &notif)
3049
3050- c.Check(ms.log.Captured(), Equals, "")
3051+ c.Check(ms.log.Captured(), Matches, "(?sm).*has no persistable card.*")
3052 }
3053
3054 func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNil(c *C) {
3055 mmu := New(ms.log)
3056 mmu.Present("app-id", "notif-id", nil)
3057- c.Check(ms.log.Captured(), Equals, "")
3058+ c.Check(ms.log.Captured(), Matches, "(?sm).*no notification.*")
3059 }
3060
3061 func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNilCard(c *C) {
3062 mmu := New(ms.log)
3063 mmu.Present("app-id", "notif-id", &launch_helper.Notification{})
3064- c.Check(ms.log.Captured(), Equals, "")
3065+ c.Check(ms.log.Captured(), Matches, "(?sm).*no notification.*")
3066 }
3067
3068=== modified file 'server/session/session_test.go'
3069--- server/session/session_test.go 2014-06-19 13:06:52 +0000
3070+++ server/session/session_test.go 2014-07-07 14:23:18 +0000
3071@@ -373,6 +373,7 @@
3072 pingTimer: time.NewTimer(pingInterval),
3073 intervalStart: now,
3074 }
3075+ time.Sleep(10 * time.Millisecond)
3076 l.pingTimer.Stop()
3077 done := l.pingTimerReset(true)
3078 c.Assert(done, Equals, true)
3079
3080=== added directory 'sounds'
3081=== added file 'sounds/sounds.go'
3082--- sounds/sounds.go 1970-01-01 00:00:00 +0000
3083+++ sounds/sounds.go 2014-07-07 14:23:18 +0000
3084@@ -0,0 +1,91 @@
3085+/*
3086+ Copyright 2014 Canonical Ltd.
3087+
3088+ This program is free software: you can redistribute it and/or modify it
3089+ under the terms of the GNU General Public License version 3, as published
3090+ by the Free Software Foundation.
3091+
3092+ This program is distributed in the hope that it will be useful, but
3093+ WITHOUT ANY WARRANTY; without even the implied warranties of
3094+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3095+ PURPOSE. See the GNU General Public License for more details.
3096+
3097+ You should have received a copy of the GNU General Public License along
3098+ with this program. If not, see <http://www.gnu.org/licenses/>.
3099+*/
3100+
3101+package sounds
3102+
3103+import (
3104+ "os"
3105+ "os/exec"
3106+ "path"
3107+
3108+ "launchpad.net/go-xdg/v0"
3109+
3110+ "launchpad.net/ubuntu-push/click"
3111+ "launchpad.net/ubuntu-push/launch_helper"
3112+ "launchpad.net/ubuntu-push/logger"
3113+)
3114+
3115+type Sound struct {
3116+ player string
3117+ log logger.Logger
3118+ dataDirs func() []string
3119+ dataFind func(string) (string, error)
3120+}
3121+
3122+func New(log logger.Logger) *Sound {
3123+ return &Sound{player: "paplay", log: log, dataDirs: xdg.Data.Dirs, dataFind: xdg.Data.Find}
3124+}
3125+
3126+func (snd *Sound) Present(appId string, nid string, notification *launch_helper.Notification) bool {
3127+ if notification == nil || notification.Sound == "" {
3128+ snd.log.Debugf("[%s] no notification or no Sound in the notification; doing nothing: %#v", nid, notification)
3129+ return false
3130+ }
3131+ absPath := snd.findSoundFile(appId, nid, notification.Sound)
3132+ if absPath == "" {
3133+ snd.log.Debugf("[%s] unable to find sound %s", nid, notification.Sound)
3134+ return false
3135+ }
3136+ snd.log.Debugf("[%s] playing sound %s using %s", nid, absPath, snd.player)
3137+ cmd := exec.Command(snd.player, absPath)
3138+ err := cmd.Start()
3139+ if err != nil {
3140+ snd.log.Debugf("[%s] unable to play: %v", nid, err)
3141+ return false
3142+ }
3143+ go func() {
3144+ err := cmd.Wait()
3145+ if err != nil {
3146+ snd.log.Debugf("[%s] error playing sound %s: %v", nid, absPath, err)
3147+ }
3148+ }()
3149+ return true
3150+}
3151+
3152+func (snd *Sound) findSoundFile(appId string, nid string, sound string) string {
3153+ parsed, err := click.ParseAppId(appId)
3154+ if err != nil {
3155+ snd.log.Debugf("[%s] no appId in %#v", nid, appId)
3156+ return ""
3157+ }
3158+ // XXX also support legacy appIds?
3159+ // first, check package-specific
3160+ absPath, err := snd.dataFind(path.Join(parsed.Package, sound))
3161+ if err == nil {
3162+ // ffffound
3163+ return absPath
3164+ }
3165+ // next, check the XDG data dirs (but skip the first one -- that's "home")
3166+ // XXX should we only check in $XDG/sounds ? (that's for sound *themes*...)
3167+ for _, dir := range snd.dataDirs()[1:] {
3168+ absPath := path.Join(dir, sound)
3169+ _, err := os.Stat(absPath)
3170+ if err == nil {
3171+ return absPath
3172+ }
3173+ }
3174+ return ""
3175+}
3176
3177=== added file 'sounds/sounds_test.go'
3178--- sounds/sounds_test.go 1970-01-01 00:00:00 +0000
3179+++ sounds/sounds_test.go 2014-07-07 14:23:18 +0000
3180@@ -0,0 +1,85 @@
3181+/*
3182+ Copyright 2014 Canonical Ltd.
3183+
3184+ This program is free software: you can redistribute it and/or modify it
3185+ under the terms of the GNU General Public License version 3, as published
3186+ by the Free Software Foundation.
3187+
3188+ This program is distributed in the hope that it will be useful, but
3189+ WITHOUT ANY WARRANTY; without even the implied warranties of
3190+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3191+ PURPOSE. See the GNU General Public License for more details.
3192+
3193+ You should have received a copy of the GNU General Public License along
3194+ with this program. If not, see <http://www.gnu.org/licenses/>.
3195+*/
3196+
3197+package sounds
3198+
3199+import (
3200+ "errors"
3201+ "os"
3202+ "path"
3203+ "testing"
3204+
3205+ . "launchpad.net/gocheck"
3206+
3207+ "launchpad.net/ubuntu-push/launch_helper"
3208+ helpers "launchpad.net/ubuntu-push/testing"
3209+)
3210+
3211+func TestSounds(t *testing.T) { TestingT(t) }
3212+
3213+type soundsSuite struct {
3214+ log *helpers.TestLogger
3215+}
3216+
3217+var _ = Suite(&soundsSuite{})
3218+
3219+func (ss *soundsSuite) SetUpTest(c *C) {
3220+ ss.log = helpers.NewTestLogger(c, "debug")
3221+}
3222+
3223+func (ss *soundsSuite) TestNew(c *C) {
3224+ s := New(ss.log)
3225+ c.Check(s.log, Equals, ss.log)
3226+ c.Check(s.player, Equals, "paplay")
3227+}
3228+
3229+func (ss *soundsSuite) TestPresent(c *C) {
3230+ s := &Sound{
3231+ player: "echo", log: ss.log,
3232+ dataFind: func(s string) (string, error) { return s, nil },
3233+ }
3234+
3235+ c.Check(s.Present("com.example.test_test", "",
3236+ &launch_helper.Notification{Sound: "hello"}), Equals, true)
3237+ c.Check(ss.log.Captured(), Matches, `(?sm).* playing sound com.example.test/hello using echo`)
3238+}
3239+
3240+func (ss *soundsSuite) TestPresentFails(c *C) {
3241+ s := &Sound{player: "/", log: ss.log}
3242+
3243+ // nil notification
3244+ c.Check(s.Present("", "", nil), Equals, false)
3245+ // no Sound
3246+ c.Check(s.Present("", "", &launch_helper.Notification{}), Equals, false)
3247+ // bad player
3248+ c.Check(s.Present("", "", &launch_helper.Notification{Sound: "hello"}), Equals, false)
3249+ s.player = "echo"
3250+ // bad app id
3251+ c.Check(s.Present("", "", &launch_helper.Notification{Sound: "hello"}), Equals, false)
3252+ s.dataFind = func(string) (string, error) { return "", errors.New("nope") }
3253+ s.dataDirs = func() []string { return []string{""} }
3254+ // no file found
3255+ c.Check(s.Present("com.example.test_test", "", &launch_helper.Notification{Sound: "hello"}), Equals, false)
3256+
3257+ // and now, just to prove it would've worked,
3258+
3259+ d := c.MkDir()
3260+ f, err := os.Create(path.Join(d, "hello"))
3261+ c.Assert(err, IsNil)
3262+ f.Close()
3263+ s.dataDirs = func() []string { return []string{"", d} }
3264+ c.Check(s.Present("com.example.test_test", "", &launch_helper.Notification{Sound: "hello"}), Equals, true)
3265+}
3266
3267=== modified file 'testing/helpers.go'
3268--- testing/helpers.go 2014-06-23 12:46:28 +0000
3269+++ testing/helpers.go 2014-07-07 14:23:18 +0000
3270@@ -20,6 +20,7 @@
3271 import (
3272 "encoding/json"
3273 "fmt"
3274+ "net/url"
3275 "os"
3276 "path"
3277 "path/filepath"
3278@@ -160,3 +161,12 @@
3279 }
3280 return res
3281 }
3282+
3283+// ParseURL parses a URL conveniently.
3284+func ParseURL(s string) *url.URL {
3285+ purl, err := url.Parse(s)
3286+ if err != nil {
3287+ panic(err)
3288+ }
3289+ return purl
3290+}
3291
3292=== modified file 'whoopsie/identifier/identifier.go'
3293--- whoopsie/identifier/identifier.go 2014-06-13 12:23:03 +0000
3294+++ whoopsie/identifier/identifier.go 2014-07-07 14:23:18 +0000
3295@@ -62,6 +62,7 @@
3296 if gerr == nil && cs != nil {
3297 goto Success
3298 }
3299+ C.g_clear_error(&gerr)
3300 time.Sleep(600 * time.Millisecond)
3301 }
3302 return errors.New("whoopsie_identifier_generate still bad after 2m; giving up")

Subscribers

People subscribed via source and target branches