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

Proposed by John Lenton
Status: Merged
Approved by: John Lenton
Approved revision: 120
Merged at revision: 117
Proposed branch: lp:~chipaca/ubuntu-push/the-push-automatic
Merge into: lp:ubuntu-push
Diff against target: 1754 lines (+1411/-133)
15 files modified
click/cblacklist/cblacklist.go (+75/-0)
click/click.go (+0/-44)
click/click_test.go (+0/-76)
client/service/postal.go (+8/-0)
client/service/postal_test.go (+26/-0)
debian/changelog (+18/-1)
debian/push-helper.hook (+1/-0)
debian/ubuntu-push-client.install (+1/-0)
docs/lowlevel.txt (+420/-0)
docs/push.svg (+439/-0)
launch_helper/cual/cual.go (+2/-1)
launch_helper/helper_finder/helper_finder.go (+107/-0)
launch_helper/helper_finder/helper_finder_test.go (+193/-0)
launch_helper/kindpool_test.go (+8/-11)
scripts/click-hook (+113/-0)
To merge this branch: bzr merge lp:~chipaca/ubuntu-push/the-push-automatic
Reviewer Review Type Date Requested Status
Ubuntu Push Hackers Pending
Review via email: mp+229306@code.launchpad.net

Description of the change

gsettings!

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'click/cblacklist'
2=== added file 'click/cblacklist/cblacklist.go'
3--- click/cblacklist/cblacklist.go 1970-01-01 00:00:00 +0000
4+++ click/cblacklist/cblacklist.go 2014-08-04 15:37:24 +0000
5@@ -0,0 +1,75 @@
6+/*
7+ Copyright 2013-2014 Canonical Ltd.
8+
9+ This program is free software: you can redistribute it and/or modify it
10+ under the terms of the GNU General Public License version 3, as published
11+ by the Free Software Foundation.
12+
13+ This program is distributed in the hope that it will be useful, but
14+ WITHOUT ANY WARRANTY; without even the implied warranties of
15+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16+ PURPOSE. See the GNU General Public License for more details.
17+
18+ You should have received a copy of the GNU General Public License along
19+ with this program. If not, see <http://www.gnu.org/licenses/>.
20+*/
21+
22+// Package cblacklist accesses the g_settings notification blacklist
23+
24+package cblacklist
25+
26+/*
27+#cgo pkg-config: gio-unix-2.0
28+#cgo pkg-config: glib-2.0
29+
30+#include <stdlib.h>
31+#include <gio/gio.h>
32+
33+#define BLACKLIST_CONFIG_SCHEMA_ID "com.ubuntu.notifications.hub"
34+#define BLACKLIST_KEY "blacklist"
35+
36+int is_blacklisted(const char *pkgname, const char *appname) {
37+ static GSettings *pushSettings = NULL;
38+ GVariantIter *iter;
39+ gchar *pkg;
40+ gchar *app;
41+ int blacklisted = 0;
42+
43+ if (!pushSettings) {
44+ GSettingsSchemaSource * source = g_settings_schema_source_get_default ();
45+ if (!g_settings_schema_source_lookup (g_settings_schema_source_get_default (), BLACKLIST_CONFIG_SCHEMA_ID, TRUE)) {
46+ return -1;
47+ }
48+ pushSettings = g_settings_new(BLACKLIST_CONFIG_SCHEMA_ID);
49+ }
50+ GVariant *blacklist = g_settings_get_value(pushSettings, BLACKLIST_KEY);
51+ g_variant_get (blacklist, "a(ss)", &iter);
52+ while (g_variant_iter_loop (iter, "(ss)", &pkg, &app)) {
53+ if (0==g_strcmp0(pkg, pkgname) && 0==g_strcmp0(app, appname)) {
54+ blacklisted = 1;
55+ break;
56+ }
57+ // No need to free pkg and app, according to GVariant array example
58+ }
59+ g_variant_iter_free (iter);
60+ g_variant_unref (blacklist);
61+ return blacklisted;
62+}
63+
64+*/
65+import "C"
66+
67+import (
68+ "unsafe"
69+
70+ "launchpad.net/ubuntu-push/click"
71+)
72+
73+// IsBlacklisted returns true if the application is in the gsettings blacklist
74+func IsBlacklisted(app *click.AppId) bool {
75+ pkgname := C.CString(app.Package)
76+ appname := C.CString(app.Application)
77+ defer C.free(unsafe.Pointer(pkgname))
78+ defer C.free(unsafe.Pointer(appname))
79+ return C.is_blacklisted(pkgname, appname) == 1
80+}
81
82=== modified file 'click/click.go'
83--- click/click.go 2014-07-25 21:50:53 +0000
84+++ click/click.go 2014-08-04 15:37:24 +0000
85@@ -22,7 +22,6 @@
86 "encoding/json"
87 "errors"
88 "fmt"
89- "io/ioutil"
90 "path/filepath"
91 "regexp"
92 "strings"
93@@ -43,9 +42,6 @@
94 original string
95 }
96
97-var hookPath = filepath.Join(xdg.Data.Home(), "ubuntu-push-client", "helpers")
98-var hookExt = ".json"
99-
100 // from https://wiki.ubuntu.com/AppStore/Interfaces/ApplicationId
101 // except the version is made optional
102 var rxClick = regexp.MustCompile(`^([a-z0-9][a-z0-9+.-]+)_([a-zA-Z0-9+.-]+)(?:_([0-9][a-zA-Z0-9.+:~-]*))?$`)
103@@ -121,46 +117,6 @@
104 }
105 }
106
107-type hookFile struct {
108- AppId string `json:"app_id"`
109- Exec string `json:"exec"`
110-}
111-
112-// Helper figures out the app id and executable of the untrusted
113-// helper for this app.
114-func (app *AppId) Helper() (helperAppId string, helperExec string) {
115- if !app.Click {
116- return "", ""
117- }
118- // xxx: should probably have a cache of this
119- matches, err := filepath.Glob(filepath.Join(hookPath, app.Package+"_*"+hookExt))
120- if err != nil {
121- return "", ""
122- }
123- var v hookFile
124- for _, m := range matches {
125- abs, err := filepath.EvalSymlinks(m)
126- if err != nil {
127- continue
128- }
129- data, err := ioutil.ReadFile(abs)
130- if err != nil {
131- continue
132- }
133- err = json.Unmarshal(data, &v)
134- if err != nil {
135- continue
136- }
137- if v.Exec != "" && (v.AppId == "" || v.AppId == app.Base()) {
138- basename := filepath.Base(m)
139- helperAppId = basename[:len(basename)-len(hookExt)]
140- helperExec = filepath.Join(filepath.Dir(abs), v.Exec)
141- return helperAppId, helperExec
142- }
143- }
144- return "", ""
145-}
146-
147 func (app *AppId) Versioned() string {
148 if app.Click {
149 if app.Version == "" {
150
151=== modified file 'click/click_test.go'
152--- click/click_test.go 2014-07-25 21:50:53 +0000
153+++ click/click_test.go 2014-08-04 15:37:24 +0000
154@@ -19,8 +19,6 @@
155 import (
156 "encoding/json"
157 "fmt"
158- "os"
159- "path/filepath"
160 "testing"
161
162 . "launchpad.net/gocheck"
163@@ -183,78 +181,4 @@
164 c.Assert(err, Equals, ErrMissingAppId)
165 c.Check(app, NotNil)
166 c.Check(app.Original(), Equals, "_non-existent-app")
167-
168-}
169-
170-type helperSuite struct {
171- oldHookPath string
172- symlinkPath string
173-}
174-
175-var _ = Suite(&helperSuite{})
176-
177-func (s *helperSuite) SetUpTest(c *C) {
178- s.oldHookPath = hookPath
179- hookPath = c.MkDir()
180- s.symlinkPath = c.MkDir()
181-}
182-
183-func (s *helperSuite) createHookfile(name string, content string) error {
184- symlink := filepath.Join(hookPath, name) + ".json"
185- filename := filepath.Join(s.symlinkPath, name)
186- f, err := os.Create(filename)
187- if err != nil {
188- return err
189- }
190- _, err = f.WriteString(content)
191- if err != nil {
192- return err
193- }
194- err = os.Symlink(filename, symlink)
195- if err != nil {
196- return err
197- }
198- return nil
199-}
200-
201-func (s *helperSuite) TearDownTest(c *C) {
202- hookPath = s.oldHookPath
203-}
204-
205-func (s *helperSuite) TestHelperBasic(c *C) {
206- c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr"}`), IsNil)
207- app, err := ParseAppId("com.example.test_test-app_1")
208- c.Assert(err, IsNil)
209- hid, hex := app.Helper()
210- c.Check(hid, Equals, "com.example.test_test-helper_1")
211- c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
212-}
213-
214-func (s *helperSuite) TestHelperFindsSpecific(c *C) {
215- // Glob() sorts, so the first one will come first
216- c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
217- c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr", "app_id": "com.example.test_test-app"}`), IsNil)
218- app, err := ParseAppId("com.example.test_test-app_1")
219- c.Assert(err, IsNil)
220- hid, hex := app.Helper()
221- c.Check(hid, Equals, "com.example.test_test-helper_1")
222- c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
223-}
224-
225-func (s *helperSuite) TestHelperCanFail(c *C) {
226- c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
227- app, err := ParseAppId("com.example.test_test-app_1")
228- c.Assert(err, IsNil)
229- hid, hex := app.Helper()
230- c.Check(hid, Equals, "")
231- c.Check(hex, Equals, "")
232-}
233-
234-func (s *clickSuite) TestHelperlegacy(c *C) {
235- appname := "ubuntu-system-settings"
236- app, err := ParseAppId("_" + appname)
237- c.Assert(err, IsNil)
238- hid, hex := app.Helper()
239- c.Check(hid, Equals, "")
240- c.Check(hex, Equals, "")
241 }
242
243=== modified file 'client/service/postal.go'
244--- client/service/postal.go 2014-07-29 15:38:04 +0000
245+++ client/service/postal.go 2014-08-04 15:37:24 +0000
246@@ -30,6 +30,7 @@
247 "launchpad.net/ubuntu-push/bus/urldispatcher"
248 "launchpad.net/ubuntu-push/bus/windowstack"
249 "launchpad.net/ubuntu-push/click"
250+ "launchpad.net/ubuntu-push/click/cblacklist"
251 "launchpad.net/ubuntu-push/launch_helper"
252 "launchpad.net/ubuntu-push/logger"
253 "launchpad.net/ubuntu-push/messaging"
254@@ -392,6 +393,8 @@
255 return svc.urlDispatcher.TestURL(app, notif.Card.Actions)
256 }
257
258+var isBlacklisted = cblacklist.IsBlacklisted
259+
260 func (svc *PostalService) messageHandler(app *click.AppId, nid string, output *launch_helper.HelperOutput) bool {
261 if output == nil || output.Notification == nil {
262 svc.Log.Debugf("skipping notification: nil.")
263@@ -403,6 +406,11 @@
264 return false
265 }
266 if !svc.windowStack.IsAppFocused(app) {
267+ if isBlacklisted(app) {
268+ svc.Log.Debugf("notification skipped (except emblem counter) because app is blacklisted")
269+ return svc.emblemCounter.Present(app, nid, output.Notification)
270+ }
271+
272 b := false
273 for _, p := range svc.Presenters {
274 // we don't want this to shortcut :)
275
276=== modified file 'client/service/postal_test.go'
277--- client/service/postal_test.go 2014-07-29 15:38:04 +0000
278+++ client/service/postal_test.go 2014-08-04 15:37:24 +0000
279@@ -141,6 +141,8 @@
280 winStackBus bus.Endpoint
281 fakeLauncher *fakeHelperLauncher
282 getTempDir func(string) (string, error)
283+ oldIsBlisted func(*click.AppId) bool
284+ blacklisted bool
285 }
286
287 type ualPostalSuite struct {
288@@ -155,6 +157,8 @@
289 var _ = Suite(&trivialPostalSuite{})
290
291 func (ps *postalSuite) SetUpTest(c *C) {
292+ ps.oldIsBlisted = isBlacklisted
293+ isBlacklisted = func(*click.AppId) bool { return ps.blacklisted }
294 ps.log = helpers.NewTestLogger(c, "debug")
295 ps.bus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true))
296 ps.notifBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true))
297@@ -163,6 +167,7 @@
298 ps.urlDispBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true))
299 ps.winStackBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []windowstack.WindowsInfo{})
300 ps.fakeLauncher = &fakeHelperLauncher{ch: make(chan []byte)}
301+ ps.blacklisted = false
302
303 ps.getTempDir = launch_helper.GetTempDir
304 d := c.MkDir()
305@@ -173,6 +178,7 @@
306 }
307
308 func (ps *postalSuite) TearDownTest(c *C) {
309+ isBlacklisted = ps.oldIsBlisted
310 launch_helper.GetTempDir = ps.getTempDir
311 }
312
313@@ -779,3 +785,23 @@
314 c.Check(err, Equals, s.err, Commentf("iter %d", i))
315 }
316 }
317+
318+func (ps *postalSuite) TestBlacklisted(c *C) {
319+ svc := ps.replaceBuses(NewPostalService(nil, ps.log))
320+ svc.Start()
321+ ps.blacklisted = false
322+
323+ emb := &launch_helper.EmblemCounter{Count: 2, Visible: true}
324+ card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Persist: true}
325+ output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: card}}
326+ embOut := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{EmblemCounter: emb}}
327+ app := clickhelp.MustParseAppId("com.example.app_app_1.0")
328+ // sanity check: things are presented as normal if blacklist == false
329+ ps.blacklisted = false
330+ c.Check(svc.messageHandler(app, "0", output), Equals, true)
331+ c.Check(svc.messageHandler(app, "1", embOut), Equals, true)
332+ ps.blacklisted = true
333+ // and regular notifications (but not emblem counters) are supprsessed if blacklisted.
334+ c.Check(svc.messageHandler(app, "2", output), Equals, false)
335+ c.Check(svc.messageHandler(app, "3", embOut), Equals, true)
336+}
337
338=== modified file 'debian/changelog'
339--- debian/changelog 2014-08-01 02:15:29 +0000
340+++ debian/changelog 2014-08-04 15:37:24 +0000
341@@ -1,3 +1,20 @@
342+ubuntu-push (0.60-0ubuntu1) UNRELEASED; urgency=medium
343+
344+ [ Guillermo Gonzalez ]
345+ * Add click hook to collect helpers data on install/update/etc and
346+ support to read the helper cached data, when available, and only
347+ refresh it when it changes.
348+ * Include notification settings cleanup in the click install hook, and
349+ rename it to click-hook
350+
351+ [ John R. Lenton ]
352+ * For the gsettings interface: Improve, add tests, make design-compliant.
353+
354+ [Roberto Alsina]
355+ * Query gsettings as to whether a notification should be presented.
356+
357+ -- John R. Lenton <john.lenton@canonical.com> Sat, 02 Aug 2014 01:42:17 +0200
358+
359 ubuntu-push (0.50+14.10.20140801-0ubuntu1) utopic; urgency=low
360
361 * New rebuild forced
362@@ -7,7 +24,7 @@
363 ubuntu-push (0.50+14.10.20140729-0ubuntu1) utopic; urgency=medium
364
365 [ Samuele Pedroni ]
366- * Cleanup and improve pos/Post tests
367+ * Cleanup and improve post/Post tests
368
369 [ Guillermo Gonzalez ]
370 * Add a loop to cleanup MessagingMenu.notifications map when the
371
372=== modified file 'debian/push-helper.hook'
373--- debian/push-helper.hook 2014-07-23 13:56:09 +0000
374+++ debian/push-helper.hook 2014-08-04 15:37:24 +0000
375@@ -1,3 +1,4 @@
376 Pattern: ${home}/.local/share/ubuntu-push-client/helpers/${id}.json
377 User-Level: yes
378 Hook-Name: push-helper
379+Exec: /usr/lib/ubuntu-push-client/click-hook
380
381=== modified file 'debian/ubuntu-push-client.install'
382--- debian/ubuntu-push-client.install 2014-07-19 00:18:18 +0000
383+++ debian/ubuntu-push-client.install 2014-08-04 15:37:24 +0000
384@@ -4,5 +4,6 @@
385 debian/exec-tool /usr/lib/${DEB_HOST_MULTIARCH}/ubuntu-app-launch/push-helper
386 debian/push-helper.hook /usr/share/click/hooks
387 signing-helper/signing-helper /usr/lib/ubuntu-push-client
388+scripts/click-hook /usr/lib/ubuntu-push-client
389 usr/bin/ubuntu-push => /usr/lib/ubuntu-push-client/ubuntu-push-client
390 scripts/software-updates-helper.py => /usr/lib/ubuntu-push-client/legacy-helpers/ubuntu-system-settings
391
392=== added directory 'docs'
393=== added file 'docs/lowlevel.txt'
394--- docs/lowlevel.txt 1970-01-01 00:00:00 +0000
395+++ docs/lowlevel.txt 2014-08-04 15:37:24 +0000
396@@ -0,0 +1,420 @@
397+Ubuntu Push Client Developer Guide
398+==================================
399+
400+:Version: 0.50+
401+
402+Introduction
403+------------
404+
405+This document describes how to use the Ubuntu Push Client service from a platform integrator's point of view.
406+Application developers are expected to use a much simpler API, in turn based on the lower-level API described here.
407+
408+The expected audience for this document is, therefore, either platform developers, or application developers who,
409+for whatever reason, can't use or prefer not to use the available higher level APIs.
410+
411+---------
412+
413+Let's describe the push system by way of an example.
414+
415+Alice has written a chat application called Chatter. Using it, Bob can send messages to Carol and viceversa. Alice has a
416+web application for it, so the way it works now is that Bob connects to the service, posts a message, and when Carol
417+connects, she gets it. If Carol leaves the browser window open, it beeps when messages arrive.
418+
419+Now Alice wants to create an Ubuntu Touch app for Chatter, so she implements the same architecture using a client that
420+does the same thing as the web browser. Sadly, since applications on Ubuntu Touch don't run continuously, messages are
421+only delivered when Carol opens the app, and the user experience suffers.
422+
423+Using the Ubuntu Push Server, this problem is alleviated: the Chatter server will deliver the messages to the Ubuntu
424+Push Server, which in turn will send it in an efficient manner to the Ubuntu Push Client running in Bob and Carol's
425+devices. The user sees a notification (all without starting the app) and then can launch it if he's interested in
426+reading messages at that point.
427+
428+Since the app is not started and messages are delivered oportunistically, this is both battery and bandwidth-efficient.
429+
430+.. figure:: push.svg
431+
432+The Ubuntu Push system provides:
433+
434+* A push server which receives **push messages** from the app servers, queues them and delivers them efficiently
435+ to the devices.
436+* A push client which receives those messages, queues messages to the app and displays notifications to the user
437+
438+The full lifecycle of a push message is:
439+
440+* Created in a application-specific server
441+* Sent to the Ubuntu Push server, targeted at a user or user+device pair
442+* Delivered to one or more Ubuntu devices
443+* Passed through the application helper for processing
444+* Notification displayed to the user (via different mechanisms)
445+* Application Message queued for the app's use
446+
447+If the user interacts with the notification, the application is launched and should check its queue for messages
448+it has to process.
449+
450+For the app developer, there are several components needed:
451+
452+* A server that sends the **push messages** to the Ubuntu Push server
453+* Support in the client app for registering with the Ubuntu Push client
454+* Support in the client app to react to **notifications** displayed to the user and process **application messages**
455+* A helper program with application-specific knowledge that transforms **push messages** as needed.
456+
457+In the following sections, we'll see how to implement all the client side parts. For the application server, see the
458+`Ubuntu Push Server API section <#ubuntu-push-server-api>`__
459+
460+The PushNotifications Service
461+-----------------------------
462+
463+:Service: com.ubuntu.PushNotifications
464+:Object path: /com/ubuntu/PushNotifications/QUOTED_PKGNAME
465+
466+The PushNotifications service handles registering the device with the Ubuntu Push service to enable delivery of messages to
467+it.
468+
469+Each Ubuntu Touch package has to use a separate object path for security reasons, that's why the object path includes QUOTED_PKGNAME.
470+For example, in the case of the music application, the package name is ``com.ubuntu.music`` and QUOTED_PKGNAME is com_2eubuntu_2emusic.
471+Everything that is not a letter or digit has to be quoted as _XX where XX are the hex digits of the character. In practice,
472+this means replacing "." with "_2e" and "-" with "_2f"
473+
474+.. note:: For applications that are not installed as part of click packages, the QUOTED_PKGNAME is "_" and the APP_ID when required is
475+ _PACKAGENAME.
476+
477+ For example, for ubuntu-system-settins:
478+
479+ * QUOTED_PKGNAME is _
480+ * APP_ID is _ubuntu-system-settings
481+
482+
483+com.ubuntu.PushNotifications.Register
484+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
485+
486+``string Register(string APP_ID)``
487+
488+Example::
489+
490+ $ gdbus call --session --dest com.ubuntu.PushNotifications --object-path /com/ubuntu/PushNotifications/com_2eubuntu_2emusic \
491+ --method com.ubuntu.PushNotifications.Register com.ubuntu.music_music
492+
493+ ('LeA4tRQG9hhEkuhngdouoA==',)
494+
495+The Register method takes as argument the APP_ID (in the example, com.ubuntu.music_music) and returns a token identifying the user
496+and device. For this to succeed the user **must** have an Ubuntu One account configured in the device.
497+
498+The APP_ID is as described in the `ApplicationId documentation <https://wiki.ubuntu.com/AppStore/Interfaces/ApplicationId>`__
499+except that the version is treated as optional. Therefore both ``com.ubuntu.music_music`` and ``com.ubuntu.music_music_1.3.496``
500+are valid. Keep in mind that while both versioned and unversioned APP_IDs are valid, they are still different and will affect
501+which notifications are delivered to the application. Unversioned IDs mean the token will be the same after updates and the application
502+will receive old notifications, while versioned IDs mean the app needs to explicitly ask to get older messages delivered.
503+
504+Register is idempotent, and calling it multiple times returns the same token.
505+
506+This token is later used by the application server to indicate the recipient of notifications.
507+
508+.. FIXME crosslink to server app
509+
510+.. note:: There is currently no way to send a push message to all of a user's devices. The application server has to send to
511+ each registered device individually instead.
512+
513+com.ubuntu.PushNotifications.Unregister
514+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
515+
516+``void Unregister(string APP_ID)``
517+
518+Example::
519+
520+ $ gdbus call --session --dest com.ubuntu.PushNotifications --object-path /com/ubuntu/PushNotifications/com_2eubuntu_2emusic \
521+ --method com.ubuntu.PushNotifications.Unregister com.ubuntu.music_music
522+
523+The Unregister method invalidates the token obtained via `Register <#com-ubuntu-pushnotifications-register>`_ therefore disabling
524+reception of push messages.
525+
526+The method takes as argument the APP_ID (in the example, com.ubuntu.music_music) and returns nothing.
527+
528+The APP_ID is as described in the `ApplicationId documentation <https://wiki.ubuntu.com/AppStore/Interfaces/ApplicationId>`__
529+except that the version is treated as optional. Therefore both ``com.ubuntu.music_music`` and ``com.ubuntu.music_music_1.3.496``
530+are valid.
531+
532+The Postal Service
533+------------------
534+
535+:Service: com.ubuntu.Postal
536+:Object path: /com/ubuntu/Postal/QUOTED_PKGNAME
537+
538+The Postal service delivers the actual messages to the applications. After the application is registered, the push client will begin
539+delivering messages to the device, which will then (possibly) cause specific notifications to be presented to the user (message bubbles,
540+sounds, haptic feedbak, etc.) Regardless of whether the user acknowledges those notifications or not, the payload of the push message
541+is put in the Postal service for the application to pick up.
542+
543+Because user response to notifications can cause application activation, apps should check the status of the Postal service every time
544+the application activates.
545+
546+com.ubuntu.Postal.Post
547+~~~~~~~~~~~~~~~~~~~~~~
548+
549+``void Post(string APP_ID, string message)``
550+
551+Example::
552+
553+ gdbus call --session --dest com.ubuntu.Postal --object-path /com/ubuntu/Postal/com_2eubuntu_2emusic \
554+ --method com.ubuntu.Postal.Post com.ubuntu.music_music \
555+ '"{\"message\": \"foobar\", \"notification\":{\"card\": {\"summary\": \"yes\", \"body\": \"hello\", \"popup\": true, \"persist\": true}}}"'
556+
557+
558+The arguments for the Post method are APP_ID (in the example, com.ubuntu.music_music) and a JSON string
559+`describing a push message. <#push-message-format>`__
560+
561+Depending on the contents of the push message it may trigger user-facing notifications, and will queue a
562+message for the app to get via the `PopAll <#com-ubuntu-postal-popalls>`__ method.
563+
564+The APP_ID is as described in the `ApplicationId documentation <https://wiki.ubuntu.com/AppStore/Interfaces/ApplicationId>`__
565+except that the version is treated as optional. Therefore both ``com.ubuntu.music_music`` and ``com.ubuntu.music_music_1.3.496``
566+are valid.
567+
568+.. note:: Post is useful as a unified frontend for notifications in Ubuntu Touch, since it wraps and abstracts several different APIs.
569+
570+com.ubuntu.Postal.PopAll
571+~~~~~~~~~~~~~~~~~~~~~~~~
572+
573+``array{string} PopAll(string APP_ID)``
574+
575+Example::
576+
577+ $ gdbus call --session --dest com.ubuntu.Postal --object-path /com/ubuntu/Postal/com_2eubuntu_2emusic \
578+ --method com.ubuntu.Postal.PopAll com.ubuntu.music_music
579+
580+ (['{"foo": "bar", ....}'],)
581+
582+The argument for the PopAll method is the APP_ID and it returns a list of strings, each string being a separate postal
583+message, the "message" element of a helper's output fed from `Post <#com-ubuntu-postal-post>`__
584+or from the Ubuntu Push service,
585+
586+Post Signal
587+~~~~~~~~~~~
588+
589+``void Post(string APP_ID)``
590+
591+Every time a notification is posted, the postal service will emit the Post signal. Your app can connect to it to react to
592+incoming notifications if it's running when they arrive. Remember that on Ubuntu Touch, the application lifecycle means
593+it will often **not** be running when notifications arrive. If the application is in the foreground when a notification
594+arrives, the notification **will not** be presented.
595+
596+The object path is similar to that of the Postal service methods, containing the QUOTED_PKGNAME.
597+
598+Persistent Notification Management
599+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
600+
601+Some notifications are persistent, meaning they don't disappear automatically. For those notifications, there is an API that
602+allows the app to manage them without having to know the underlying details of the platform.
603+
604+On each notification there's an optional ``tag`` field, used for this purpose.
605+
606+``array(string) ListPersistent(string APP_ID)``
607+
608+Returns a list of the tags of notifications with the "persist" element set to true that are visible to the user right now.
609+
610+``void ClearPersistent(string APP_ID, [tag1, tag2,....])``
611+
612+Clears persistent notifications for that app by tag(s). If none given, match all.
613+
614+``void SetCounter(string APP_ID, int count int, bool visible)``
615+
616+Set the counter to the given values.
617+
618+
619+Application Helpers
620+-------------------
621+
622+The payload delivered to push-client will be passed onto a helper program that can modify it as needed before passing it onto
623+the postal service (see `Helper Output Format <#helper-output-format>`__).
624+
625+The helper receives two arguments ``infile`` and ``outfile``. The message is delivered via ``infile`` and the transformed
626+version is placed in ``outfile``.
627+
628+This is the simplest possible useful helper, which simply passes the message through unchanged::
629+
630+ #!/usr/bin/python3
631+
632+ import sys
633+ f1, f2 = sys.argv[1:3]
634+ open(f2, "w").write(open(f1).read())
635+
636+Helpers need to be added to the click package manifest::
637+
638+ {
639+ "name": "com.ubuntu.developer.ralsina.hello",
640+ "description": "description of hello",
641+ "framework": "ubuntu-sdk-14.10-qml-dev2",
642+ "architecture": "all",
643+ "title": "hello",
644+ "hooks": {
645+ "hello": {
646+ "apparmor": "hello.json",
647+ "desktop": "hello.desktop"
648+ },
649+ "helloHelper": {
650+ "apparmor": "helloHelper-apparmor.json",
651+ "push-helper": "helloHelper.json"
652+ }
653+ },
654+ "version": "0.2",
655+ "maintainer": "Roberto Alsina <roberto.alsina@canonical.com>"
656+ }
657+
658+Here, we created a helloHelper entry in hooks that has an apparmor profile and an additional JSON file for the push-helper hook.
659+
660+helloHelper-apparmor.json must contain **only** the push-notification-client policy group::
661+
662+ {
663+ "policy_groups": [
664+ "push-notification-client"
665+ ],
666+ "policy_version": 1.2
667+ }
668+
669+And helloHelper.json must have at least a exec key with the path to the helper executable relative to the json, and optionally
670+an app_id key containing the short id of one of the apps in the package (in the format packagename_appname without a version).
671+If the app_id is not specified, the helper will be used for all apps in the package::
672+
673+ {
674+ "exec": "helloHelper",
675+ "app_id": "com.ubuntu.developer.ralsina.hello_hello"
676+ }
677+
678+.. note:: For deb packages, helpers should be installed into /usr/lib/ubuntu-push-client/legacy-helpers/ as part of the package.
679+
680+Helper Output Format
681+--------------------
682+
683+Helpers output has two parts, the postal message (in the "message" key) and a notification to be presented to the user (in the "notification" key).
684+
685+Here's a simple example::
686+
687+ {
688+ "message": "foobar",
689+ "notification": {
690+ "tag": "foo",
691+ "card": {
692+ "summary": "yes",
693+ "body": "hello",
694+ "popup": true,
695+ "persist": true
696+ }
697+ "sound": "buzz.mp3",
698+ "vibrate": {
699+ "pattern": [200, 100],
700+ "duration": 200,
701+ "repeat": 2
702+ }
703+ "emblem-counter": {
704+ "count": 12,
705+ "visible": true
706+ }
707+ }
708+ }
709+
710+The notification can contain a **tag** field, which can later be used by the `persistent notification management API. <#persistent-notification-management>`__
711+
712+:message: (optional) A JSON object that is passed as-is to the application via PopAll.
713+:notification: (optional) Describes the user-facing notifications triggered by this push message.
714+
715+The notification can contain a **card**. A card describes a specific notification to be given to the user,
716+and has the following fields:
717+
718+:summary: (required) a title. The card will not be presented if this is missing.
719+:body: longer text, defaults to empty.
720+:actions: If empty (the default), a bubble notification is non-clickable.
721+ If you add a URL, then bubble notifications are clickable and launch that URL. One use for this is using a URL like
722+ ``appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version`` which will switch to the app or launch
723+ it if it's not running. See `URLDispatcher <https://wiki.ubuntu.com/URLDispatcher>`__ for more information.
724+
725+:icon: An icon relating to the event being notified. Defaults to empty (no icon);
726+ a secondary icon relating to the application will be shown as well, regardless of this field.
727+:timestamp: Seconds since the unix epoch, only used for persist (for now)
728+:persist: Whether to show in notification centre; defaults to false
729+:popup: Whether to show in a bubble. Users can disable this, and can easily miss them, so don't rely on it exclusively. Defaults to false.
730+
731+.. note:: Keep in mind that the precise way in which each field is presented to the user depends on factors such as
732+ whether it's shown as a bubble or in the notification centre, or even the version of Ubuntu Touch the user
733+ has on their device.
734+
735+The notification can contain a **sound** field. This is the path to a sound file. The user can disable it, so don't rely on it exclusively.
736+Defaults to empty (no sound). This is a relative path, and will be looked up in (a) the application's .local/share/<pkgname>, and (b)
737+standard xdg dirs.
738+
739+The notification can contain a **vibrate** field, causing haptic feedback, that has the following content:
740+
741+:pattern: a list of integers describing a vibration pattern.
742+:duration: duration in milliseconds. Is equivalent to setting pattern to [duration], and overrides pattern.
743+:repeat: number of times the pattern has to be repeated (defaults to 1, 0 is the same as 1).
744+
745+The notification can contain a **emblem-counter** field, with the following content:
746+
747+:count: a number to be displayed over the application's icon in the launcher.
748+:visible: set to true to show the counter, or false to hide it.
749+
750+.. note:: Unlike other notifications, emblem-counter needs to be cleaned by the app itself.
751+ Please see `the persistent notification management section. <#persistent-notification-management>`__
752+
753+.. FIXME crosslink to hello example app on each method
754+
755+Security
756+~~~~~~~~
757+
758+To use the push API, applications need to request permission in their security profile, using something like this::
759+
760+ {
761+ "policy_groups": [
762+ "networking",
763+ "push-notification-client"
764+ ],
765+ "policy_version": 1.2
766+ }
767+
768+
769+Ubuntu Push Server API
770+----------------------
771+
772+The Ubuntu Push server is located at https://push.ubuntu.com and has a single endpoint: ``/notify``.
773+To notify a user, your application has to do a POST with ``Content-type: application/json``.
774+
775+Here is an example of the POST body using all the fields::
776+
777+ {
778+ "appid": "com.ubuntu.music_music",
779+ "expire_on": "2014-10-08T14:48:00.000Z",
780+ "token": "LeA4tRQG9hhEkuhngdouoA==",
781+ "clear_pending": true,
782+ "replace_tag": "tagname",
783+ "data": {
784+ "message": "foobar",
785+ "notification": {
786+ "card": {
787+ "summary": "yes",
788+ "body": "hello",
789+ "popup": true,
790+ "persist": true
791+ }
792+ "sound": "buzz.mp3",
793+ "tag": "foo",
794+ "vibrate": {
795+ "duration": 200,
796+ "pattern": (200, 100),
797+ "repeat": 2
798+ }
799+ "emblem-counter": {
800+ "count": 12,
801+ "visible": true
802+ }
803+ }
804+ }
805+ }
806+
807+
808+:appid: ID of the application that will receive the notification, as described in the client side documentation.
809+:expire_on: Expiration date/time for this message, in `ISO8601 Extendend format <http://en.wikipedia.org/wiki/ISO_8601>`__
810+:token: The token identifying the user+device to which the message is directed, as described in the client side documentation.
811+:clear_pending: Discards all previous pending notifications. Usually in response to getting a "too-many-pending" error.
812+:replace_tag: If there's a pending notification with the same tag, delete it before queuing this new one.
813+:data: A JSON object.
814+
815+In this example, data is `what a helper would output <#helper-output-format>`__ but that's not necessarily the case.
816+The content of the data field will be passed to the helper application which **has** to produce output in that format.
817
818=== added file 'docs/push.svg'
819--- docs/push.svg 1970-01-01 00:00:00 +0000
820+++ docs/push.svg 2014-08-04 15:37:24 +0000
821@@ -0,0 +1,439 @@
822+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
823+<svg
824+ xmlns:dc="http://purl.org/dc/elements/1.1/"
825+ xmlns:cc="http://creativecommons.org/ns#"
826+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
827+ xmlns:svg="http://www.w3.org/2000/svg"
828+ xmlns="http://www.w3.org/2000/svg"
829+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
830+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
831+ width="41cm"
832+ height="41cm"
833+ viewBox="381 56 811 811"
834+ id="svg2"
835+ version="1.1"
836+ inkscape:version="0.48.4 r9939"
837+ sodipodi:docname="push.svg">
838+ <metadata
839+ id="metadata88">
840+ <rdf:RDF>
841+ <cc:Work
842+ rdf:about="">
843+ <dc:format>image/svg+xml</dc:format>
844+ <dc:type
845+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
846+ </cc:Work>
847+ </rdf:RDF>
848+ </metadata>
849+ <defs
850+ id="defs86">
851+ <marker
852+ inkscape:stockid="Arrow1Lstart"
853+ orient="auto"
854+ refY="0.0"
855+ refX="0.0"
856+ id="Arrow1Lstart"
857+ style="overflow:visible">
858+ <path
859+ id="path3984"
860+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
861+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
862+ transform="scale(0.8) translate(12.5,0)" />
863+ </marker>
864+ <marker
865+ inkscape:stockid="Arrow1Lend"
866+ orient="auto"
867+ refY="0.0"
868+ refX="0.0"
869+ id="Arrow1Lend"
870+ style="overflow:visible;">
871+ <path
872+ id="path3987"
873+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
874+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;"
875+ transform="scale(0.8) rotate(180) translate(12.5,0)" />
876+ </marker>
877+ </defs>
878+ <sodipodi:namedview
879+ pagecolor="#ffffff"
880+ bordercolor="#666666"
881+ borderopacity="1"
882+ objecttolerance="10"
883+ gridtolerance="10"
884+ guidetolerance="10"
885+ inkscape:pageopacity="0"
886+ inkscape:pageshadow="2"
887+ inkscape:window-width="1855"
888+ inkscape:window-height="1056"
889+ id="namedview84"
890+ showgrid="false"
891+ showguides="true"
892+ inkscape:guide-bbox="true"
893+ inkscape:zoom="0.62854741"
894+ inkscape:cx="311.03462"
895+ inkscape:cy="472.71845"
896+ inkscape:window-x="65"
897+ inkscape:window-y="24"
898+ inkscape:window-maximized="1"
899+ inkscape:current-layer="svg2">
900+ <sodipodi:guide
901+ orientation="0,1"
902+ position="820.94047,984.81037"
903+ id="guide4555" />
904+ </sodipodi:namedview>
905+ <line
906+ style="fill:none;fill-opacity:0;stroke-width:2;stroke-dasharray:4;stroke:#000000;stroke-opacity:0.3037037"
907+ x1="786"
908+ y1="57"
909+ x2="786"
910+ y2="865"
911+ id="line10" />
912+ <line
913+ style="fill:none;fill-opacity:0;stroke-width:2;stroke-dasharray:4;stroke:#000000;stroke-opacity:0.3037037"
914+ x1="382"
915+ y1="461"
916+ x2="1190"
917+ y2="461"
918+ id="line12" />
919+ <g
920+ id="g6336"
921+ transform="translate(695.42763,-47.960526)">
922+ <rect
923+ transform="matrix(0.55824934,0,0,0.55824934,381,56)"
924+ y="124.296"
925+ x="-641.16089"
926+ height="87.503342"
927+ width="240.23645"
928+ id="rect6311"
929+ style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:0.3037037;stroke-dasharray:none" />
930+ <text
931+ id="text14"
932+ y="154.5843"
933+ x="53.172604"
934+ style="font-size:12.79979992px;font-style:normal;font-weight:normal;text-anchor:start;fill:#000000;font-family:sans-serif"
935+ font-size="12.7998">
936+ <tspan
937+ id="tspan16"
938+ y="154.5843"
939+ x="53.172604">Server Side</tspan>
940+ </text>
941+ </g>
942+ <g
943+ id="g6331"
944+ transform="translate(679.44079,315.29605)">
945+ <rect
946+ y="473.10199"
947+ x="39.503288"
948+ height="48.848682"
949+ width="134.11185"
950+ id="rect6311-7"
951+ style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.67474806;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:0.3037037;stroke-dasharray:none" />
952+ <text
953+ id="text18"
954+ y="502.29813"
955+ x="72.128502"
956+ style="font-size:12.79979992px;font-style:normal;font-weight:normal;text-anchor:start;fill:#000000;font-family:sans-serif"
957+ font-size="12.7998">
958+ <tspan
959+ id="tspan20"
960+ y="502.29813"
961+ x="72.128502">Client Side</tspan>
962+ </text>
963+ </g>
964+ <g
965+ id="g30"
966+ transform="translate(-13.351976,0)">
967+ <rect
968+ style="fill:#ffffff"
969+ x="467.5"
970+ y="594"
971+ width="238"
972+ height="152"
973+ id="rect32" />
974+ <rect
975+ style="fill:none;stroke:#000000;stroke-width:2"
976+ x="467.5"
977+ y="594"
978+ width="238"
979+ height="152"
980+ id="rect34" />
981+ </g>
982+ <text
983+ font-size="12.7998"
984+ style="font-size:12.79979992px;font-style:normal;font-weight:normal;text-anchor:start;fill:#000000;font-family:sans-serif"
985+ x="535.41882"
986+ y="674.77179"
987+ id="text36">
988+ <tspan
989+ x="535.41882"
990+ y="674.77179"
991+ id="tspan38">Push Client</tspan>
992+ </text>
993+ <g
994+ id="g3102"
995+ transform="translate(-26.16447,-47.960526)">
996+ <g
997+ id="g40">
998+ <rect
999+ id="rect42"
1000+ height="84"
1001+ width="205"
1002+ y="725"
1003+ x="909.5"
1004+ style="fill:#ffffff" />
1005+ <rect
1006+ id="rect44"
1007+ height="84"
1008+ width="205"
1009+ y="725"
1010+ x="909.5"
1011+ style="fill:none;stroke:#000000;stroke-width:2" />
1012+ </g>
1013+ <text
1014+ id="text46"
1015+ y="770.53119"
1016+ x="972.96625"
1017+ style="font-size:12.79979992px;font-style:normal;font-weight:normal;text-anchor:start;fill:#000000;font-family:sans-serif"
1018+ font-size="12.7998">
1019+ <tspan
1020+ id="tspan48"
1021+ y="770.53119"
1022+ x="972.96625">Push Helper</tspan>
1023+ </text>
1024+ </g>
1025+ <g
1026+ id="g3972"
1027+ transform="translate(-10.46581,-2.6644735)">
1028+ <g
1029+ transform="translate(-15.098684,-4.4407892)"
1030+ id="g3095">
1031+ <g
1032+ id="g50">
1033+ <rect
1034+ style="fill:#ffffff"
1035+ x="908.90002"
1036+ y="534.70001"
1037+ width="205"
1038+ height="84"
1039+ id="rect52" />
1040+ <rect
1041+ style="fill:none;stroke:#000000;stroke-width:2"
1042+ x="908.90002"
1043+ y="534.70001"
1044+ width="205"
1045+ height="84"
1046+ id="rect54" />
1047+ </g>
1048+ <text
1049+ font-size="12.7998"
1050+ style="font-size:12.79979992px;font-style:normal;font-weight:normal;text-anchor:start;fill:#000000;font-family:sans-serif"
1051+ x="976.13806"
1052+ y="580.2312"
1053+ id="text56">
1054+ <tspan
1055+ x="976.13806"
1056+ y="580.2312"
1057+ id="tspan58">Application</tspan>
1058+ </text>
1059+ </g>
1060+ </g>
1061+ <text
1062+ xml:space="preserve"
1063+ style="font-size:15.63098145px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"
1064+ x="825.89801"
1065+ y="214.20395"
1066+ id="text4424"
1067+ sodipodi:linespacing="125%"><tspan
1068+ sodipodi:role="line"
1069+ id="tspan4426"
1070+ x="825.89801"
1071+ y="214.20395">/notify</tspan></text>
1072+ <g
1073+ id="g4546"
1074+ transform="translate(8.2993444,1.4703926)">
1075+ <g
1076+ transform="translate(-21.65132,-445.13816)"
1077+ id="g30-8-0">
1078+ <rect
1079+ id="rect32-6-7"
1080+ height="152"
1081+ width="238"
1082+ y="594"
1083+ x="467.5"
1084+ style="fill:#ffffff" />
1085+ <rect
1086+ id="rect34-62-9"
1087+ height="152"
1088+ width="238"
1089+ y="594"
1090+ x="467.5"
1091+ style="fill:none;stroke:#000000;stroke-width:2" />
1092+ </g>
1093+ <text
1094+ font-size="12.7998"
1095+ style="font-size:12.79979992px;font-style:normal;font-weight:normal;text-anchor:start;fill:#000000;font-family:sans-serif"
1096+ x="528.01489"
1097+ y="229.63364"
1098+ id="text80-9">
1099+ <tspan
1100+ x="528.01489"
1101+ y="229.63364"
1102+ id="tspan82-1">PushServer</tspan>
1103+ </text>
1104+ </g>
1105+ <g
1106+ id="g4994">
1107+ <g
1108+ transform="translate(399.33553,-442.47368)"
1109+ id="g30-8-0-2">
1110+ <rect
1111+ id="rect32-6-7-0"
1112+ height="152"
1113+ width="238"
1114+ y="594"
1115+ x="467.5"
1116+ style="fill:#ffffff" />
1117+ <rect
1118+ id="rect34-62-9-0"
1119+ height="152"
1120+ width="238"
1121+ y="594"
1122+ x="467.5"
1123+ style="fill:none;stroke:#000000;stroke-width:2" />
1124+ </g>
1125+ <text
1126+ font-size="12.7998"
1127+ style="font-size:12.79979992px;font-style:normal;font-weight:normal;text-anchor:start;fill:#000000;font-family:sans-serif"
1128+ x="950.25482"
1129+ y="232.29811"
1130+ id="text80-9-1">
1131+ <tspan
1132+ x="950.25482"
1133+ y="232.29811"
1134+ id="tspan82-1-7">App Server</tspan>
1135+ </text>
1136+ </g>
1137+ <path
1138+ style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none;marker-end:url(#Arrow1Lend)"
1139+ d="M 870.28411,306.64076 557.36389,305.73534"
1140+ id="path5001"
1141+ inkscape:connector-type="polyline"
1142+ inkscape:connector-curvature="0"
1143+ inkscape:connection-start="#g4994"
1144+ inkscape:connection-start-point="d4"
1145+ inkscape:connection-end="#g4546"
1146+ inkscape:connection-end-point="d4"
1147+ transform="matrix(0.55824934,0,0,0.55824934,381,56)" />
1148+ <path
1149+ style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:url(#Arrow1Lstart);marker-end:url(#Arrow1Lend)"
1150+ d="m 1083.4505,443.39743 0,401.37697"
1151+ id="path5003"
1152+ inkscape:connector-type="polyline"
1153+ inkscape:connector-curvature="0"
1154+ inkscape:connection-start="#g4994"
1155+ inkscape:connection-start-point="d4"
1156+ inkscape:connection-end="#g3972"
1157+ inkscape:connection-end-point="d4"
1158+ transform="matrix(0.55824934,0,0,0.55824934,381,56)" />
1159+ <path
1160+ style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:url(#Arrow1Lstart);marker-end:none"
1161+ d="M 899.8408,964.68112 557.36389,1048.0044"
1162+ id="path5005"
1163+ inkscape:connector-type="polyline"
1164+ inkscape:connector-curvature="0"
1165+ inkscape:connection-start="#g3972"
1166+ inkscape:connection-start-point="d4"
1167+ inkscape:connection-end="#g30"
1168+ inkscape:connection-end-point="d4"
1169+ transform="matrix(0.55824934,0,0,0.55824934,381,56)" />
1170+ <path
1171+ style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none;marker-end:url(#Arrow1Lend)"
1172+ d="m 344.19749,441.25844 0,522.46863"
1173+ id="path5009"
1174+ inkscape:connector-type="polyline"
1175+ inkscape:connector-curvature="0"
1176+ inkscape:connection-start="#g4546"
1177+ inkscape:connection-start-point="d4"
1178+ inkscape:connection-end="#g30"
1179+ inkscape:connection-end-point="d4"
1180+ transform="matrix(0.55824934,0,0,0.55824934,381,56)" />
1181+ <text
1182+ xml:space="preserve"
1183+ style="font-size:15.63098145px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"
1184+ x="825.62708"
1185+ y="589.00653"
1186+ id="text6709"
1187+ sodipodi:linespacing="125%"><tspan
1188+ sodipodi:role="line"
1189+ id="tspan6711"
1190+ x="825.62708"
1191+ y="589.00653">PopAll</tspan></text>
1192+ <text
1193+ xml:space="preserve"
1194+ style="font-size:15.63098145px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"
1195+ x="491.13156"
1196+ y="412.44592"
1197+ id="text6717"
1198+ sodipodi:linespacing="125%"><tspan
1199+ sodipodi:role="line"
1200+ id="tspan6719"
1201+ x="491.13156"
1202+ y="412.44592">Push Notifications</tspan></text>
1203+ <text
1204+ xml:space="preserve"
1205+ style="font-size:15.63098145px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"
1206+ x="1041.7894"
1207+ y="410.93091"
1208+ id="text6721"
1209+ sodipodi:linespacing="125%"><tspan
1210+ sodipodi:role="line"
1211+ id="tspan6723"
1212+ x="1041.7894"
1213+ y="410.93091">App-specific</tspan></text>
1214+ <text
1215+ xml:space="preserve"
1216+ style="font-size:22.32997322px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
1217+ x="459.1579"
1218+ y="104.07236"
1219+ id="text3045"
1220+ sodipodi:linespacing="125%"><tspan
1221+ sodipodi:role="line"
1222+ id="tspan3047"
1223+ x="459.1579"
1224+ y="104.07236">Ubuntu Push System</tspan></text>
1225+ <text
1226+ xml:space="preserve"
1227+ style="font-size:22.32997322px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
1228+ x="919.22369"
1229+ y="101.4079"
1230+ id="text3049"
1231+ sodipodi:linespacing="125%"><tspan
1232+ sodipodi:role="line"
1233+ id="tspan3051"
1234+ x="919.22369"
1235+ y="101.4079">Application</tspan></text>
1236+ <path
1237+ style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;marker-end:url(#Arrow1Lend)"
1238+ d="m 556.83946,1145.6987 c 0,0 254.55518,138.4143 340.46755,62.0478"
1239+ id="path3051"
1240+ inkscape:connector-curvature="0"
1241+ transform="matrix(0.55824934,0,0,0.55824934,381,56)"
1242+ sodipodi:nodetypes="cc" />
1243+ <path
1244+ sodipodi:nodetypes="cc"
1245+ inkscape:connector-curvature="0"
1246+ id="path3053"
1247+ d="m 882.80916,712.85627 c 0,0 -142.10524,-77.26969 -190.06574,-34.63814"
1248+ style="fill:none;stroke:#000000;stroke-width:1.67474802000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;marker-end:url(#Arrow1Lend)" />
1249+ <text
1250+ xml:space="preserve"
1251+ style="font-size:15.63098145px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"
1252+ x="811.3623"
1253+ y="717.78949"
1254+ id="text4019"
1255+ sodipodi:linespacing="125%"><tspan
1256+ sodipodi:role="line"
1257+ id="tspan4021"
1258+ x="811.3623"
1259+ y="717.78949">Call</tspan></text>
1260+</svg>
1261
1262=== modified file 'launch_helper/cual/cual.go'
1263--- launch_helper/cual/cual.go 2014-07-18 20:45:21 +0000
1264+++ launch_helper/cual/cual.go 2014-08-04 15:37:24 +0000
1265@@ -31,6 +31,7 @@
1266 "unsafe"
1267
1268 "launchpad.net/ubuntu-push/click"
1269+ "launchpad.net/ubuntu-push/launch_helper/helper_finder"
1270 "launchpad.net/ubuntu-push/logger"
1271 )
1272
1273@@ -77,7 +78,7 @@
1274 }
1275
1276 func (hs *helperState) HelperInfo(app *click.AppId) (string, string) {
1277- return app.Helper()
1278+ return helper_finder.Helper(app, hs.log)
1279 }
1280
1281 func (hs *helperState) Launch(appId, exec, f1, f2 string) (string, error) {
1282
1283=== added directory 'launch_helper/helper_finder'
1284=== added file 'launch_helper/helper_finder/helper_finder.go'
1285--- launch_helper/helper_finder/helper_finder.go 1970-01-01 00:00:00 +0000
1286+++ launch_helper/helper_finder/helper_finder.go 2014-08-04 15:37:24 +0000
1287@@ -0,0 +1,107 @@
1288+package helper_finder
1289+
1290+import (
1291+ "encoding/json"
1292+ "io/ioutil"
1293+ "os"
1294+ "path/filepath"
1295+ "sync"
1296+ "time"
1297+
1298+ "launchpad.net/go-xdg/v0"
1299+
1300+ "launchpad.net/ubuntu-push/click"
1301+ "launchpad.net/ubuntu-push/logger"
1302+)
1303+
1304+type helperValue struct {
1305+ HelperId string `json:"helper_id"`
1306+ Exec string `json:"exec"`
1307+}
1308+
1309+type hookFile struct {
1310+ AppId string `json:"app_id"`
1311+ Exec string `json:"exec"`
1312+}
1313+
1314+var mapLock sync.Mutex
1315+var helpersInfo = make(map[string]helperValue)
1316+var helpersDataMtime time.Time
1317+
1318+var helpersDataPath = filepath.Join(xdg.Data.Home(), "ubuntu-push-client", "helpers_data.json")
1319+var hookPath = filepath.Join(xdg.Data.Home(), "ubuntu-push-client", "helpers")
1320+var hookExt = ".json"
1321+
1322+// helperFromHookfile figures out the app id and executable of the untrusted
1323+// helper for this app.
1324+func helperFromHookFile(app *click.AppId) (helperAppId string, helperExec string) {
1325+ matches, err := filepath.Glob(filepath.Join(hookPath, app.Package+"_*"+hookExt))
1326+ if err != nil {
1327+ return "", ""
1328+ }
1329+ var v hookFile
1330+ for _, m := range matches {
1331+ abs, err := filepath.EvalSymlinks(m)
1332+ if err != nil {
1333+ continue
1334+ }
1335+ data, err := ioutil.ReadFile(abs)
1336+ if err != nil {
1337+ continue
1338+ }
1339+ err = json.Unmarshal(data, &v)
1340+ if err != nil {
1341+ continue
1342+ }
1343+ if v.Exec != "" && (v.AppId == "" || v.AppId == app.Base()) {
1344+ basename := filepath.Base(m)
1345+ helperAppId = basename[:len(basename)-len(hookExt)]
1346+ helperExec = filepath.Join(filepath.Dir(abs), v.Exec)
1347+ return helperAppId, helperExec
1348+ }
1349+ }
1350+ return "", ""
1351+}
1352+
1353+// Helper figures out the id and executable of the untrusted
1354+// helper for this app.
1355+func Helper(app *click.AppId, log logger.Logger) (helperAppId string, helperExec string) {
1356+ if !app.Click {
1357+ return "", ""
1358+ }
1359+ fInfo, err := os.Stat(helpersDataPath)
1360+ if err != nil {
1361+ // cache file is missing, go via the slow route
1362+ log.Infof("Cache file not found, falling back to .json file lookup")
1363+ return helperFromHookFile(app)
1364+ }
1365+ // get the lock as the map can be changed while we read
1366+ mapLock.Lock()
1367+ defer mapLock.Unlock()
1368+ if helpersInfo == nil || fInfo.ModTime().After(helpersDataMtime) {
1369+ data, err := ioutil.ReadFile(helpersDataPath)
1370+ if err != nil {
1371+ return "", ""
1372+ }
1373+ err = json.Unmarshal(data, &helpersInfo)
1374+ if err != nil {
1375+ return "", ""
1376+ }
1377+ helpersDataMtime = fInfo.ModTime()
1378+ }
1379+ var info helperValue
1380+ info, ok := helpersInfo[app.Base()]
1381+ if !ok {
1382+ // ok, appid wasn't there, try with the package
1383+ info, ok = helpersInfo[app.Package]
1384+ if !ok {
1385+ return "", ""
1386+ }
1387+ }
1388+ if info.Exec != "" {
1389+ helperAppId = info.HelperId
1390+ helperExec = info.Exec
1391+ return helperAppId, helperExec
1392+ }
1393+ return "", ""
1394+}
1395
1396=== added file 'launch_helper/helper_finder/helper_finder_test.go'
1397--- launch_helper/helper_finder/helper_finder_test.go 1970-01-01 00:00:00 +0000
1398+++ launch_helper/helper_finder/helper_finder_test.go 2014-08-04 15:37:24 +0000
1399@@ -0,0 +1,193 @@
1400+package helper_finder
1401+
1402+import (
1403+ "os"
1404+ "path/filepath"
1405+ "testing"
1406+ "time"
1407+
1408+ . "launchpad.net/gocheck"
1409+ helpers "launchpad.net/ubuntu-push/testing"
1410+
1411+ "launchpad.net/ubuntu-push/click"
1412+)
1413+
1414+type helperSuite struct {
1415+ oldHookPath string
1416+ symlinkPath string
1417+ oldHelpersDataPath string
1418+ log *helpers.TestLogger
1419+}
1420+
1421+func TestHelperFinder(t *testing.T) { TestingT(t) }
1422+
1423+var _ = Suite(&helperSuite{})
1424+
1425+func (s *helperSuite) SetUpTest(c *C) {
1426+ s.oldHookPath = hookPath
1427+ hookPath = c.MkDir()
1428+ s.symlinkPath = c.MkDir()
1429+ s.oldHelpersDataPath = helpersDataPath
1430+ helpersDataPath = filepath.Join(c.MkDir(), "helpers_data.json")
1431+ s.log = helpers.NewTestLogger(c, "debug")
1432+}
1433+
1434+func (s *helperSuite) createHookfile(name string, content string) error {
1435+ symlink := filepath.Join(hookPath, name) + ".json"
1436+ filename := filepath.Join(s.symlinkPath, name)
1437+ f, err := os.Create(filename)
1438+ if err != nil {
1439+ return err
1440+ }
1441+ _, err = f.WriteString(content)
1442+ if err != nil {
1443+ return err
1444+ }
1445+ err = os.Symlink(filename, symlink)
1446+ if err != nil {
1447+ return err
1448+ }
1449+ return nil
1450+}
1451+
1452+func (s *helperSuite) createHelpersDatafile(content string) error {
1453+ f, err := os.Create(helpersDataPath)
1454+ if err != nil {
1455+ return err
1456+ }
1457+ _, err = f.WriteString(content)
1458+ if err != nil {
1459+ return err
1460+ }
1461+ return nil
1462+}
1463+
1464+func (s *helperSuite) TearDownTest(c *C) {
1465+ hookPath = s.oldHookPath
1466+ os.Remove(helpersDataPath)
1467+ helpersDataPath = s.oldHelpersDataPath
1468+ helpersDataMtime = time.Now().Add(-1 * time.Hour)
1469+ helpersInfo = nil
1470+}
1471+
1472+func (s *helperSuite) TestHelperBasic(c *C) {
1473+ c.Assert(s.createHelpersDatafile(`{"com.example.test": {"helper_id": "com.example.test_test-helper_1", "exec": "tsthlpr"}}`), IsNil)
1474+ app, err := click.ParseAppId("com.example.test_test-app_1")
1475+ c.Assert(err, IsNil)
1476+ hid, hex := Helper(app, s.log)
1477+ c.Check(hid, Equals, "com.example.test_test-helper_1")
1478+ c.Check(hex, Equals, "tsthlpr")
1479+}
1480+
1481+func (s *helperSuite) TestHelperFindsSpecific(c *C) {
1482+ fileContent := `{"com.example.test_test-other-app": {"exec": "aaaaaaa", "helper_id": "com.example.test_aaaa-helper_1"},
1483+ "com.example.test_test-app": {"exec": "tsthlpr", "helper_id": "com.example.test_test-helper_1"}}`
1484+ c.Assert(s.createHelpersDatafile(fileContent), IsNil)
1485+
1486+ app, err := click.ParseAppId("com.example.test_test-app_1")
1487+ c.Assert(err, IsNil)
1488+ hid, hex := Helper(app, s.log)
1489+ c.Check(hid, Equals, "com.example.test_test-helper_1")
1490+ c.Check(hex, Equals, "tsthlpr")
1491+}
1492+
1493+func (s *helperSuite) TestHelperCanFail(c *C) {
1494+ fileContent := `{"com.example.test_test-other-app": {"exec": "aaaaaaa", "helper_id": "com.example.test_aaaa-helper_1"}}`
1495+ c.Assert(s.createHelpersDatafile(fileContent), IsNil)
1496+ app, err := click.ParseAppId("com.example.test_test-app_1")
1497+ c.Assert(err, IsNil)
1498+ hid, hex := Helper(app, s.log)
1499+ c.Check(hid, Equals, "")
1500+ c.Check(hex, Equals, "")
1501+}
1502+
1503+func (s *helperSuite) TestHelperFailInvalidJson(c *C) {
1504+ fileContent := `{invalid json"com.example.test_test-other-app": {"exec": "aaaaaaa", "helper_id": "com.example.test_aaaa-helper_1"}}`
1505+ c.Assert(s.createHelpersDatafile(fileContent), IsNil)
1506+ app, err := click.ParseAppId("com.example.test_test-app_1")
1507+ c.Assert(err, IsNil)
1508+ hid, hex := Helper(app, s.log)
1509+ c.Check(hid, Equals, "")
1510+ c.Check(hex, Equals, "")
1511+}
1512+
1513+func (s *helperSuite) TestHelperFailMissingExec(c *C) {
1514+ fileContent := `{"com.example.test_test-app": {"helper_id": "com.example.test_aaaa-helper_1"}}`
1515+ c.Assert(s.createHelpersDatafile(fileContent), IsNil)
1516+ app, err := click.ParseAppId("com.example.test_test-app_1")
1517+ c.Assert(err, IsNil)
1518+ hid, hex := Helper(app, s.log)
1519+ c.Check(hid, Equals, "")
1520+ c.Check(hex, Equals, "")
1521+}
1522+
1523+func (s *helperSuite) TestHelperlegacy(c *C) {
1524+ appname := "ubuntu-system-settings"
1525+ app, err := click.ParseAppId("_" + appname)
1526+ c.Assert(err, IsNil)
1527+ hid, hex := Helper(app, s.log)
1528+ c.Check(hid, Equals, "")
1529+ c.Check(hex, Equals, "")
1530+}
1531+
1532+// Missing Cache file test
1533+
1534+func (s *helperSuite) TestHelperMissingCacheFile(c *C) {
1535+ c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr"}`), IsNil)
1536+ app, err := click.ParseAppId("com.example.test_test-app_1")
1537+ c.Assert(err, IsNil)
1538+ hid, hex := Helper(app, s.log)
1539+ c.Check(hid, Equals, "com.example.test_test-helper_1")
1540+ c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
1541+ c.Check(s.log.Captured(), Matches, ".*Cache file not found, falling back to .json file lookup\n")
1542+}
1543+
1544+func (s *helperSuite) TestHelperFromHookBasic(c *C) {
1545+ c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr"}`), IsNil)
1546+ app, err := click.ParseAppId("com.example.test_test-app_1")
1547+ c.Assert(err, IsNil)
1548+ hid, hex := Helper(app, s.log)
1549+ c.Check(hid, Equals, "com.example.test_test-helper_1")
1550+ c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
1551+}
1552+
1553+func (s *helperSuite) TestHelperFromHookFindsSpecific(c *C) {
1554+ // Glob() sorts, so the first one will come first
1555+ c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
1556+ c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr", "app_id": "com.example.test_test-app"}`), IsNil)
1557+ app, err := click.ParseAppId("com.example.test_test-app_1")
1558+ c.Assert(err, IsNil)
1559+ hid, hex := Helper(app, s.log)
1560+ c.Check(hid, Equals, "com.example.test_test-helper_1")
1561+ c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
1562+}
1563+
1564+func (s *helperSuite) TestHelperFromHookCanFail(c *C) {
1565+ c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
1566+ app, err := click.ParseAppId("com.example.test_test-app_1")
1567+ c.Assert(err, IsNil)
1568+ hid, hex := Helper(app, s.log)
1569+ c.Check(hid, Equals, "")
1570+ c.Check(hex, Equals, "")
1571+}
1572+
1573+func (s *helperSuite) TestHelperFromHookInvalidJson(c *C) {
1574+ c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `invalid json {"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
1575+ app, err := click.ParseAppId("com.example.test_test-app_1")
1576+ c.Assert(err, IsNil)
1577+ hid, hex := Helper(app, s.log)
1578+ c.Check(hid, Equals, "")
1579+ c.Check(hex, Equals, "")
1580+}
1581+
1582+func (s *helperSuite) TestHelperFromHooFailBrokenSymlink(c *C) {
1583+ name := "com.example.test_aaaa-helper_1"
1584+ c.Assert(s.createHookfile(name, `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
1585+ filename := filepath.Join(s.symlinkPath, name)
1586+ os.Remove(filename)
1587+ app, err := click.ParseAppId("com.example.test_test-app_1")
1588+ c.Assert(err, IsNil)
1589+ hid, hex := Helper(app, s.log)
1590+ c.Check(hid, Equals, "")
1591+ c.Check(hex, Equals, "")
1592+}
1593
1594=== modified file 'launch_helper/kindpool_test.go'
1595--- launch_helper/kindpool_test.go 2014-07-29 15:24:01 +0000
1596+++ launch_helper/kindpool_test.go 2014-08-04 15:37:24 +0000
1597@@ -542,31 +542,28 @@
1598 }
1599
1600 func (s *poolSuite) TestBigBacklogShrinks(c *C) {
1601+ oldBufSz := InputBufferSize
1602+ InputBufferSize = 0
1603+ defer func() { InputBufferSize = oldBufSz }()
1604 s.pool.(*kindHelperPool).maxNum = 1
1605 ch := s.pool.Start()
1606 defer s.pool.Stop()
1607- numBad := 10
1608
1609 app := clickhelp.MustParseAppId("com.example.test_test-app")
1610 s.pool.Run("fake", &HelperInput{App: app, NotificationId: "0", Payload: []byte(`""`)})
1611-
1612- for i := 0; i < numBad; i++ {
1613- s.pool.Run("NOT-THERE", &HelperInput{App: app})
1614- }
1615 s.pool.Run("fake", &HelperInput{App: app, NotificationId: "1", Payload: []byte(`""`)})
1616 s.pool.Run("fake", &HelperInput{App: app, NotificationId: "2", Payload: []byte(`""`)})
1617 s.waitForArgs(c, "Launch")
1618 go s.fakeLauncher.done("0")
1619- // now we should get the fake + all the bad ones
1620- for i := 0; i < numBad+1; i++ {
1621- takeNext(ch, c)
1622- }
1623+ takeNext(ch, c)
1624+ // so now there's one done, one "running", and one more waiting.
1625+ // kicking it forward one more notch before checking the logs:
1626 s.waitForArgs(c, "Launch")
1627 go s.fakeLauncher.done("1")
1628 takeNext(ch, c)
1629- // so now there's one good one "running", and one more waiting.
1630+ // (two done, one "running")
1631 c.Check(s.log.Captured(), Matches, `(?ms).* shrunk to 1 entries\.$`)
1632- // and the shrinker shrunk
1633+ // and the backlog shrinker shrunk the backlog
1634 c.Check(s.log.Captured(), Matches, `(?ms).*copying backlog to avoid wasting too much space .*`)
1635 }
1636
1637
1638=== added file 'scripts/click-hook'
1639--- scripts/click-hook 1970-01-01 00:00:00 +0000
1640+++ scripts/click-hook 2014-08-04 15:37:24 +0000
1641@@ -0,0 +1,113 @@
1642+#!/usr/bin/python3
1643+"""Collect helpers hook data into a single json file"""
1644+
1645+import argparse
1646+import json
1647+import os
1648+import sys
1649+import time
1650+
1651+import xdg.BaseDirectory
1652+
1653+from gi.repository import GLib
1654+from gi.repository import Gio
1655+from gi.repository import Click
1656+
1657+hook_ext = '.json'
1658+
1659+
1660+def tup2variant(tup):
1661+ builder = GLib.VariantBuilder.new(GLib.VariantType.new("(ss)"))
1662+ builder.add_value(GLib.Variant.new_string(tup[0]))
1663+ builder.add_value(GLib.Variant.new_string(tup[1]))
1664+ return builder.end()
1665+
1666+
1667+def cleanup_settings():
1668+ clickdb = Click.DB.new()
1669+ clickdb.read()
1670+
1671+ pkgnames = []
1672+ for package in clickdb.get_packages(False):
1673+ pkgnames.append(package.get_property('package'))
1674+
1675+ settings = Gio.Settings.new('com.ubuntu.notifications.hub')
1676+ goodapps = GLib.VariantBuilder.new(GLib.VariantType.new("a(ss)"))
1677+
1678+ for appname in settings.get_value('blacklist').unpack():
1679+ if appname[0] in pkgnames:
1680+ goodapps.add_value(tup2variant(appname))
1681+ elif (appname[0] == appname[1]):
1682+ appinfo = Gio.DesktopAppInfo.new(appname[0] + ".desktop")
1683+ if not appinfo is None:
1684+ goodapps.add_value(tup2variant(appname))
1685+
1686+ settings.set_value('blacklist', goodapps.end())
1687+
1688+
1689+def collect_helpers(helpers_data_path, helpers_data_path_tmp, hooks_path):
1690+ helpers_data = {}
1691+ for hook_fname in os.listdir(hooks_path):
1692+ if not hook_fname.endswith(hook_ext):
1693+ continue
1694+ try:
1695+ with open(os.path.join(hooks_path, hook_fname), 'r') as fd:
1696+ data = json.load(fd)
1697+ except Exception:
1698+ continue
1699+ else:
1700+ helper_id = os.path.splitext(hook_fname)[0]
1701+ exec_path = data['exec']
1702+ if exec_path != "":
1703+ realpath = os.path.realpath(os.path.join(hooks_path,
1704+ hook_fname))
1705+ exec_path = os.path.join(os.path.dirname(realpath), exec_path)
1706+ app_id = data.get('app_id', None)
1707+ if app_id is None:
1708+ # no app_id, use the package name from the helper_id
1709+ app_id = helper_id.split('_')[0]
1710+ elif app_id.count('_') >= 3:
1711+ # remove the version from the app_id
1712+ app_id = app_id.rsplit('_', 1)[0]
1713+ helpers_data[app_id] = {'exec': exec_path, 'helper_id': helper_id}
1714+
1715+ # write the collected data to a temp file and rename the original once
1716+ # everything is on disk
1717+ try:
1718+ tmp_filename = helpers_data_path_tmp % (time.time(),)
1719+ with open(tmp_filename, 'w') as dest:
1720+ json.dump(helpers_data, dest)
1721+ dest.flush()
1722+ os.rename(tmp_filename, helpers_data_path)
1723+ except Exception:
1724+ return True
1725+ return False
1726+
1727+
1728+def main(helpers_data_path=None, helpers_data_path_tmp=None, hooks_path=None):
1729+ collect_fail = collect_helpers(helpers_data_path, helpers_data_path_tmp,
1730+ hooks_path)
1731+ clean_settings_fail = False
1732+ try:
1733+ cleanup_settings()
1734+ except Exception:
1735+ clean_settings_fail = True
1736+ return int(collect_fail or clean_settings_fail)
1737+
1738+
1739+if __name__ == "__main__":
1740+ xdg_data_home = xdg.BaseDirectory.xdg_data_home
1741+ parser = argparse.ArgumentParser(description=__doc__)
1742+ parser.add_argument('-d', '--data-home',
1743+ help='The Path to the (xdg) data home',
1744+ default=xdg_data_home)
1745+ args = parser.parse_args()
1746+ xdg_data_home = args.data_home
1747+ helpers_data_path = os.path.join(xdg_data_home, 'ubuntu-push-client',
1748+ 'helpers_data.json')
1749+ helpers_data_path_tmp = os.path.join(xdg_data_home, 'ubuntu-push-client',
1750+ '.helpers_data_%s.tmp')
1751+ hooks_path = os.path.join(xdg_data_home, 'ubuntu-push-client', 'helpers')
1752+ sys.exit(main(helpers_data_path=helpers_data_path,
1753+ helpers_data_path_tmp=helpers_data_path_tmp,
1754+ hooks_path=hooks_path))

Subscribers

People subscribed via source and target branches