~ubuntuone-control-tower/ubuntuone-client/stable-1-0

2 by Rodney Dawes
Import the code
1
#!/usr/bin/python
2
3
# ubuntuone-client-applet - Tray icon applet for managing Ubuntu One
4
#
5
# Author: Rodney Dawes <rodney.dawes@canonical.com>
6
#
7
# Copyright 2009 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
from __future__ import with_statement
22
23
import pygtk
24
pygtk.require('2.0')
25
import gobject
26
import gtk
27
import os
28
import subprocess
29
import sys
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
30
from gettext import gettext as _
2 by Rodney Dawes
Import the code
31
32
import dbus.service
33
34
# pylint: disable-msg=F0401
35
import pynotify
36
37
from dbus.exceptions import DBusException
38
from dbus.mainloop.glib import DBusGMainLoop
39
from canonical.ubuntuone.oauthdesktop.main import Login
40
from gobject.option import OptionGroup, OptionParser, make_option
41
from xdg.BaseDirectory import xdg_data_home, xdg_config_home
42
from urllib import quote
43
44
from canonical.ubuntuone.oauthdesktop.logger import setupLogging
45
setupLogging()
46
import logging
47
logger = logging.getLogger("UbuntuOne.Client.Applet")
48
49
DBusGMainLoop(set_as_default=True)
50
51
# Wait for 30 seconds to get a proper login request
52
LOGIN_TIMEOUT = 30
53
54
# Prepend tooltip labels with this label
55
TOOLTIP_TITLE = "Ubuntu One: "
56
57
DBUS_IFACE_NAME = "com.ubuntuone.SyncDaemon"
58
DBUS_IFACE_SYNC_NAME = "com.ubuntuone.SyncDaemon.SyncDaemon"
59
DBUS_IFACE_STATUS_NAME = "com.ubuntuone.SyncDaemon.Status"
60
61
class AppletMain(object):
62
      """Main applet process class."""
63
64
      def __init__(self, signup=False, *args, **kw):
65
            """Initializes the child threads and dbus monitor."""
66
            from twisted.internet import gtk2reactor
67
            gtk2reactor.install()
68
            login = Login(dbus.service.BusName("com.ubuntuone.Authentication",
69
                                               bus=dbus.SessionBus()))
70
71
            self.__signup = signup
72
            self.__icon = None
73
            self.__check_id = 0
74
75
            self.__bus = dbus.SessionBus()
76
            self.__bus.add_signal_receiver(
77
                  handler_function=self.__new_credentials,
78
                  signal_name="NewCredentials",
79
                  dbus_interface="com.ubuntuone.Authentication")
80
            self.__bus.add_signal_receiver(
81
                  handler_function=self.__auth_denied,
82
                  signal_name="AuthorizationDenied",
83
                  dbus_interface="com.ubuntuone.Authentication")
84
85
      def __new_credentials(self, realm=None, consumer_key=None, sender=None):
86
            """Signal callback for when we get new credentials."""
87
            self.__start_storage_daemon()
88
89
            if not self.__icon:
90
                  self.__icon = AppletIcon()
91
            if self.__check_id > 0:
92
                  gobject.source_remove(self.__check_id)
93
            if self.__signup:
94
                  self.add_to_autostart()
95
96
      def __auth_denied(self):
97
            """Signal callback for when auth was denied by user."""
98
            def timeout_exit():
99
                  """Do the quit on a timeout"""
100
                  from twisted.internet import reactor
101
                  reactor.stop()
102
                  return False
103
104
            self.remove_from_autostart()
105
            if not self.__signup:
106
                  self.__check_id = gobject.timeout_add_seconds(LOGIN_TIMEOUT,
107
                                                                timeout_exit)
108
109
      def __check_for_token(self):
110
            """Method to check for an existing token."""
111
            try:
112
                  client = self.__bus.get_object("com.ubuntuone.Authentication",
113
                                                 "/")
114
            except DBusException:
115
                  return False
116
117
            iface = dbus.Interface(client, "com.ubuntuone.Authentication")
118
            def handler():
119
                  """Simple handler to make dbus do stuff async."""
120
                  pass
121
122
            iface.maybe_login("https://ubuntuone.com", "ubuntuone",
123
                              self.__signup,
124
                              reply_handler=handler, error_handler=handler)
125
            return False
126
127
      def __start_storage_daemon_maybe(self):
128
            """Start the storage daemon."""
129
            try:
130
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
131
            except DBusException:
132
                  return False
133
134
            iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
135
            def handler(data):
136
                  """Simple handler to make dbus to stuff async."""
137
                  pass
138
139
            iface.get_rootdir(reply_handler=handler, error_handler=handler)
140
            return False
141
142
      def __start_storage_daemon(self):
143
            """Need to call dbus from a idle callback"""
144
            gobject.idle_add(self.__start_storage_daemon_maybe)
145
146
      def add_to_autostart(self):
147
            """Add ourself to the autostart config."""
148
            autostart_entry = """[Desktop Entry]
149
Name=Ubuntu One
150
Comment=Control applet for Ubuntu One
151
Exec=ubuntuone-client-applet
152
Icon=ubuntuone-client
153
Terminal=false
154
Type=Application
155
X-Ubuntu-Gettext-Domain=ubuntuone-client
156
X-KDE-autostart-after=panel
157
X-GNOME-Autostart-enabled=true
158
"""
159
            with open(os.path.join(xdg_config_home, "autostart",
160
                                   "ubuntuone-client-applet.desktop"),
161
                      "w+") as f:
162
                  f.write(autostart_entry)
163
164
      def remove_from_autostart(self):
165
            """Remove ourself from the autostart config."""
166
            path = os.path.join(xdg_config_home, "autostart",
167
                                "ubuntuone-client-applet.desktop")
168
            try:
169
                  os.unlink(path)
170
            except OSError:
171
                  pass
172
173
      def main(self):
174
            """Starts the gtk main loop."""
175
            from twisted.internet import reactor
176
            gobject.idle_add(self.__check_for_token)
177
178
            reactor.run()
179
180
181
def do_xdg_open(path_or_url):
182
      """Utility method to run xdg-open with path_or_url."""
183
      ret = subprocess.call(["xdg-open", path_or_url])
184
      if ret != 0:
185
            logger.debug("Failed to run 'xdg-open %s'" % path_or_url)
186
187
class AppletIcon(gtk.StatusIcon):
188
      """
189
      Custom StatusIcon derived from gtk.StatusIcon which supports
190
      animated icons and a few other nice things.
191
      """
192
193
      def __init__(self, *args, **kw):
194
            """Initializes our custom StatusIcon based widget."""
195
            super(AppletIcon, self).__init__(*args, **kw)
196
197
            self.__managed_dir = None
198
199
            self.__theme = gtk.icon_theme_get_default()
200
            iconpath = os.path.abspath(os.path.join(
201
                        os.path.split(os.path.dirname(__file__))[0],
202
                        "data"))
203
            self.__theme.append_search_path(iconpath)
204
205
            self.__theme.append_search_path(os.path.sep + os.path.join(
206
                        "usr", "share", "ubuntuone-client", "icons"))
207
            self.__theme.append_search_path(os.path.sep + os.path.join(
208
                        "usr", "local", "share", "ubuntuone-client", "icons"))
209
210
            self.set_from_icon_name('ubuntuone-client')
211
            self.connect("popup-menu", self.__popup_menu)
212
            self.connect("activate", self.__open_folder)
213
214
            self.__busy = False
215
            self.__busy_id = 0
216
217
            self.__size = 24
218
            self.__pixbuf = None
219
220
            self.__vframes = 0
221
            self.__hframes = 0
222
            self.__vpos = 0
223
            self.__hpos = 0
224
225
            self.__size_changed(self, self.__size)
226
227
            self.__items = {}
228
            self.__menu = self.__build_menus()
229
230
            self.__connected = False
231
            self.__need_update = False
232
233
            pynotify.init("Ubuntu One")
234
235
            self.__bus = dbus.SessionBus()
236
237
            self.__bus.add_signal_receiver(
238
                  handler_function=self.__status_changed,
239
                  signal_name="StatusChanged",
240
                  dbus_interface=DBUS_IFACE_STATUS_NAME)
241
242
            gobject.idle_add(self.__get_root)
243
244
      def __status_changed(self, status):
245
            """The sync daemon status changed."""
246
            if self.__managed_dir is None:
247
                gobject.idle_add(self.__get_root)
248
            if str(status) == "OFFLINE" or str(status).startswith("INIT") or \
249
                      str(status).startswith("READY"):
250
                  self.set_busy(False)
251
                  self.set_from_icon_name("ubuntuone-client-offline")
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
252
                  self.set_tooltip(TOOLTIP_TITLE + _("Disconnected"))
2 by Rodney Dawes
Import the code
253
                  self.__connected = False
254
            elif str(status).startswith("IDLE"):
255
                  self.set_busy(False)
256
                  self.set_from_icon_name("ubuntuone-client")
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
257
                  self.set_tooltip(TOOLTIP_TITLE + _("Idle"))
2 by Rodney Dawes
Import the code
258
                  self.__connected = True
259
            elif str(status) == "BAD_VERSION":
260
                  self.set_busy(False)
261
                  self.set_from_icon_name("ubuntuone-client-offline")
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
262
                  self.set_tooltip(TOOLTIP_TITLE + _("Update Required"))
2 by Rodney Dawes
Import the code
263
                  self.__connected = False
264
                  self.__need_update = True
265
                  self.__items["connect"].set_sensitive(False)
266
                  n = pynotify.Notification(
267
                        "Ubuntu One",
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
268
                        _("A new client version is required to continue " +
2 by Rodney Dawes
Import the code
269
                        "using the service. Please click the Ubuntu One" +
5.3.6 by Rodrigo Moya
Fixed misplaced paren
270
                        "applet icon to upgrade."),
271
                        "ubuntuone-client-offline")
2 by Rodney Dawes
Import the code
272
                  n.set_urgency(pynotify.URGENCY_CRITICAL)
273
                  n.show()
274
                  logger.debug("Invalid protocol version. Update needed.")
275
            elif str(status) == "AUTH_FAILED":
276
                  self.set_busy(False)
277
                  self.set_from_icon_name("ubuntuone-client-offline")
278
                  self.set_tooltip(TOOLTIP_TITLE + "Authentication failed")
279
                  self.__connected = False
280
                  try:
281
                        client = self.__bus.get_object(
282
                              "com.ubuntuone.Authentication",
283
                              "/")
284
                        iface = dbus.Interface(client,
285
                                               "com.ubuntuone.Authentication")
286
                        def handler():
287
                              """Simple handler to make dbus do stuff async."""
288
                              return
289
290
                        iface.maybe_login("https://ubuntuone.com",
291
                                          "ubuntuone",
292
                                          True,
293
                                          reply_handler=handler,
294
                                          error_handler=handler)
295
296
                  except DBusException, e:
297
                        raise e
298
299
            else:
300
                  self.set_busy()
301
                  self.__connected = True
302
                  if str(status).startswith("CONNECTING") or \
303
                            str(status).startswith("AUTHENTICATING") or \
304
                            str(status).startswith("CONNECTED"):
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
305
                        self.set_tooltip(TOOLTIP_TITLE + _("Connecting"))
2 by Rodney Dawes
Import the code
306
                  elif str(status).startswith("SCANNING") or \
307
                            str(status).startswith("READING"):
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
308
                        self.set_tooltip(TOOLTIP_TITLE + _("Scanning"))
2 by Rodney Dawes
Import the code
309
                  elif str(status).startswith("WORKING"):
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
310
                        self.set_tooltip(TOOLTIP_TITLE + _("Synchronizing"))
2 by Rodney Dawes
Import the code
311
                  elif str(status).startswith("UNKNOWN_ERROR"):
312
                        pass
313
                  else:
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
314
                        self.set_tooltip(TOOLTIP_TITLE + _("Working"))
2 by Rodney Dawes
Import the code
315
316
            if self.__connected:
317
                  self.__items["connect"].hide()
318
                  self.__items["disconnect"].show()
319
            else:
320
                  self.__items["connect"].show()
321
                  self.__items["disconnect"].hide()
322
323
      def __get_root(self):
324
            """Method to get the rootdir from the sync daemon."""
325
            # Get the managed root directory
326
            try:
327
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
328
            except DBusException:
329
                  return False
330
331
            iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
332
            def got_root(root):
333
                  """We got the root dir."""
334
                  if self.__managed_dir:
335
                        self.__remove_from_places(self.__managed_dir)
336
                  self.__managed_dir = root
337
                  if os.path.isdir(self.__managed_dir) and \
338
                            os.access(self.__managed_dir,
339
                                      os.F_OK | os.R_OK | os.W_OK):
340
                        self.__items["open"].set_sensitive(True)
341
                        self.__add_to_places()
342
                  else:
343
                        self.__items["open"].set_sensitive(False)
344
345
            def got_err(error):
346
                  """Handle error from the dbus callback."""
347
                  if self.__managed_dir:
348
                        self.__remove_from_places(self.__managed_dir)
349
                  self.__managed_dir = None
350
                  self.__items["open"].set_sensitive(False)
351
352
            iface.get_rootdir(reply_handler=got_root, error_handler=got_err)
353
354
            # Now get the current status
355
            try:
356
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/status")
357
            except DBusException:
358
                  return False
359
360
            iface = dbus.Interface(client, DBUS_IFACE_STATUS_NAME)
361
            def status_error(error):
362
                  """Handle status error."""
363
                  pass
364
365
            iface.current_status(reply_handler=self.__status_changed,
366
                                 error_handler=status_error)
367
368
            return False
369
370
      def __build_menus(self):
371
            """Create the pop-up menu items."""
372
            menu = gtk.Menu()
373
374
            self.__items["connect"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_CONNECT)
375
            menu.append(self.__items["connect"])
376
            self.__items["connect"].connect("activate", self.__toggle_state)
377
            self.__items["connect"].show()
378
379
            self.__items["disconnect"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_DISCONNECT)
380
            menu.append(self.__items["disconnect"])
381
            self.__items["disconnect"].connect("activate", self.__toggle_state)
382
383
            sep = gtk.SeparatorMenuItem()
384
            menu.append(sep)
385
            sep.show()
386
5.3.1 by Rodrigo Moya
Added initial support for translations in LP
387
            self.__items["bug"] = gtk.MenuItem(label=_("_Report a Problem"))
2 by Rodney Dawes
Import the code
388
            menu.append(self.__items["bug"])
389
            self.__items["bug"].connect("activate", self.__open_website,
390
                    "https://bugs.launchpad.net/ubuntuone-client/+filebug")
391
            self.__items["bug"].show()
392
5.3.1 by Rodrigo Moya
Added initial support for translations in LP
393
            self.__items["open"] = gtk.MenuItem(label=_("_Open Folder"))
2 by Rodney Dawes
Import the code
394
            menu.append(self.__items["open"])
395
            self.__items["open"].connect("activate", self.__open_folder)
396
            self.__items["open"].set_sensitive(False)
397
            self.__items["open"].show()
398
5.3.1 by Rodrigo Moya
Added initial support for translations in LP
399
            self.__items["web"] = gtk.MenuItem(label=_("_Go to Web"))
2 by Rodney Dawes
Import the code
400
            menu.append(self.__items["web"])
401
            self.__items["web"].connect("activate", self.__open_website)
402
            self.__items["web"].show()
403
404
            sep = gtk.SeparatorMenuItem()
405
            menu.append(sep)
406
            sep.show()
407
408
            self.__items["quit"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT)
409
            menu.append(self.__items["quit"])
410
            self.__items["quit"].connect("activate", self.__quit_applet)
411
            self.__items["quit"].show()
412
413
            menu.show()
414
415
            return menu
416
417
      def __size_changed(self, icon, size, data=None):
418
            """Callback for when the size changes."""
419
            if size < 24:
420
                  self.__size = 16
421
            elif size >= 24 and size < 32:
422
                  self.__size = 24
423
            elif size >= 32 and size < 48:
424
                  self.__size = 32
425
            elif size >= 48 and size < 64:
426
                  self.__size = 48
427
            else:
428
                  self.__size = size
429
430
            self.__pixbuf = self.__theme.load_icon(
431
                  'ubuntuone-client-working',
432
                  self.__size,
433
                  gtk.ICON_LOOKUP_GENERIC_FALLBACK)
434
435
            self.__hframes = self.__pixbuf.get_width() / self.__size
436
            self.__vframes = self.__pixbuf.get_height() / self.__size
437
438
            return True
439
440
441
      def __spinner_timeout(self):
442
            """Timeout callback that spins the spinner."""
443
            # This is wrong
444
            if not self.__busy:
445
                  return False
446
447
            # Skip the first (resting) frame to avoid flicker
448
            if self.__hpos == 0 and self.__vpos == 0:
449
                  self.__hpos = 1
450
451
            x = self.__size * self.__hpos
452
            y = self.__size * self.__vpos
453
            pixbuf = self.__pixbuf.subpixbuf(x, y, self.__size, self.__size)
454
            self.set_from_pixbuf(pixbuf)
455
456
            self.__hpos += 1
457
            if self.__hpos == self.__hframes:
458
                  self.__hpos = 0
459
                  self.__vpos += 1
460
            if self.__vpos >= self.__vframes:
461
                  self.__vpos = 0
462
463
            return True
464
465
      def set_busy(self, busy=True):
466
            if self.__busy and busy:
467
                  return
468
469
            if not busy and self.__busy_id:
470
                  gobject.source_remove(self.__busy_id)
471
472
            self.__busy = busy
473
            self.__busy_id = gobject.timeout_add(100,
474
                                                 self.__spinner_timeout)
475
476
      def __popup_menu(self, icon, button, timestamp, data=None):
477
            """Pops up the context menu for the tray icon."""
478
            self.__menu.popup(None, None, gtk.status_icon_position_menu,
479
                              button, timestamp, icon)
480
481
      def __quit_applet(self, menuitem, data=None):
482
            """Disconnects the daemon and closes the applet."""
483
            def handler():
484
                  """Simple handler to make dbus do stuff async."""
485
                  pass
486
487
            try:
488
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
489
                  iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
490
                  iface.disconnect(reply_handler=handler, error_handler=handler)
491
            except DBusException:
492
                  pass
493
494
            from twisted.internet import reactor
495
            reactor.stop()
496
497
      def __toggle_state(self, menuitem, data=None):
498
            """Connects or disconnects the storage sync process."""
499
            def handler():
500
                  """Simple handler to make dbus do stuff async."""
501
                  pass
502
503
            try:
504
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
505
            except DBusException:
506
                  return
507
508
            iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
509
            if self.__connected:
510
                  iface.disconnect(reply_handler=handler, error_handler=handler)
511
            else:
512
                  iface.connect(reply_handler=handler, error_handler=handler)
513
514
      def __open_folder(self, data=None):
515
            """Opens the storage folder in the file manager."""
516
            if self.__need_update:
517
                  do_xdg_open("apt:ubuntuone-storage-protocol?refresh=yes")
518
                  return
519
520
            if not self.__managed_dir or not os.path.isdir(self.__managed_dir):
521
                  return
522
523
            folder = "file://%s" % quote(self.__managed_dir)
524
            do_xdg_open(folder)
525
526
      def __open_website(self, data=None, url=None):
527
            """Opens the ubuntuone.com web site."""
528
            if url:
529
                  do_xdg_open(url)
530
            else:
531
                  do_xdg_open("https://ubuntuone.com/")
532
533
      def __add_to_places(self):
534
            """Add the managed directory to the .gtk-bookmarks file."""
535
            path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
536
            with open(path, "a+") as f:
537
                  bookmarks_entry = "file://%s %s\n" % (
538
                        quote(self.__managed_dir),
539
                        os.path.basename(self.__managed_dir))
540
                  in_file = False
541
                  for line in f:
542
                        if line == bookmarks_entry:
543
                              in_file = True
544
                  if not in_file:
545
                        f.write(bookmarks_entry)
546
547
      def __remove_from_places(self, dir):
548
            """Remove the old path from the .gtk-bookmarks file."""
549
            path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
550
            with open(path, "a+") as f:
551
                  entry = "file://%s %s\n" % (quote(dir), os.path.basename(dir))
552
                  lines = []
553
                  for line in f:
554
                        if line != entry:
555
                              lines.append(line)
556
                  f.truncate(0)
557
                  f.seek(0)
558
                  output = "".join(lines)
559
                  f.write(output)
560
561
562
if __name__ == "__main__":
563
      parser = OptionParser("",
564
                            description="Ubuntu One status applet",
565
                            option_list = [
566
                  make_option("--signup", "-s",
567
                              action="store_true",
568
                              dest="signup",
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
569
                              help=_("Log in or Sign Up for Ubuntu One")),
2 by Rodney Dawes
Import the code
570
                  ])
571
      options, args = parser.parse_args()
572
573
      # Register DBus service for making sure we run only one instance
574
      bus = dbus.SessionBus()
575
      if bus.request_name("com.ubuntuone.ClientApplet", dbus.bus.NAME_FLAG_DO_NOT_QUEUE) == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
5.3.3 by Rodrigo Moya
* bin/ubuntuone-client-applet: fixed gettext import and marked missing strings
576
            print _("Ubuntu One client applet already running, quitting")
2 by Rodney Dawes
Import the code
577
            sys.exit(-1)
578
579
      icon = AppletMain(signup=options.signup)
580
      icon.main()