FIxing not clearing the number on sending sms
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @bug Crashes when switching contact lists, see http://talk.maemo.org/showpost.php?p=312920&postcount=176
22 @bug Refeshing SMS a lot, then go to contacts and send a message, see http://talk.maemo.org/showpost.php?p=312920&postcount=176
23 @bug Can't send sms from dialpad, see http://talk.maemo.org/showpost.php?p=312922&postcount=177
24 @bug Sending an sms from contacts gave an error
25 @bug Getting into a bad state on connection loss, see http://talk.maemo.org/showpost.php?p=312912&postcount=175
26
27 @todo Figure out how to integrate with the Maemo contacts app
28 @bug Session timeouts are bad, possible solutions:
29         @li For every X minutes, if logged in, attempt login
30         @li Restructure so code handling login/dial/sms is beneath everything else and login attempts are made if those fail
31 @todo Add logging support to make debugging issues for people a lot easier
32 """
33
34
35 from __future__ import with_statement
36
37 import sys
38 import gc
39 import os
40 import threading
41 import base64
42 import ConfigParser
43 import itertools
44 import warnings
45
46 import gtk
47 import gtk.glade
48
49 try:
50         import hildon
51 except ImportError:
52         hildon = None
53
54 import constants
55 import gtk_toolbox
56
57
58 def getmtime_nothrow(path):
59         try:
60                 return os.path.getmtime(path)
61         except StandardError:
62                 return 0
63
64
65 def display_error_message(msg):
66         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
67
68         def close(dialog, response):
69                 dialog.destroy()
70         error_dialog.connect("response", close)
71         error_dialog.run()
72
73
74 class Dialcentral(object):
75
76         _glade_files = [
77                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
78                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
79                 '/usr/lib/dialcentral/dialcentral.glade',
80         ]
81
82         KEYPAD_TAB = 0
83         RECENT_TAB = 1
84         MESSAGES_TAB = 2
85         CONTACTS_TAB = 3
86         ACCOUNT_TAB = 4
87
88         NULL_BACKEND = 0
89         GC_BACKEND = 1
90         GV_BACKEND = 2
91         BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
92
93         def __init__(self):
94                 self._initDone = False
95                 self._connection = None
96                 self._osso = None
97                 self._clipboard = gtk.clipboard_get()
98
99                 self._credentials = ("", "")
100                 self._selectedBackendId = self.NULL_BACKEND
101                 self._defaultBackendId = self.GC_BACKEND
102                 self._phoneBackends = None
103                 self._dialpads = None
104                 self._accountViews = None
105                 self._messagesViews = None
106                 self._recentViews = None
107                 self._contactsViews = None
108                 self._alarmHandler = None
109                 self._ledHandler = None
110                 self._originalCurrentLabels = []
111
112                 for path in self._glade_files:
113                         if os.path.isfile(path):
114                                 self._widgetTree = gtk.glade.XML(path)
115                                 break
116                 else:
117                         display_error_message("Cannot find dialcentral.glade")
118                         gtk.main_quit()
119                         return
120
121                 self._window = self._widgetTree.get_widget("mainWindow")
122                 self._notebook = self._widgetTree.get_widget("notebook")
123                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
124                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
125
126                 self._app = None
127                 self._isFullScreen = False
128                 if hildon is not None:
129                         self._app = hildon.Program()
130                         oldWindow = self._window
131                         self._window = hildon.Window()
132                         oldWindow.get_child().reparent(self._window)
133                         self._app.add_window(self._window)
134
135                         try:
136                                 self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
137                                 self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
138                                 self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
139                         except TypeError, e:
140                                 warnings.warn(e.message)
141                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
142                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('message_scrolledwindow'), True)
143                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
144
145                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
146                         menu = gtk.Menu()
147                         for child in gtkMenu.get_children():
148                                 child.reparent(menu)
149                         self._window.set_menu(menu)
150                         gtkMenu.destroy()
151
152                         self._window.connect("key-press-event", self._on_key_press)
153                         self._window.connect("window-state-event", self._on_window_state_change)
154                 else:
155                         pass # warnings.warn("No Hildon", UserWarning, 2)
156
157                 # If under hildon, rely on the application name being shown
158                 if hildon is None:
159                         self._window.set_title("%s" % constants.__pretty_app_name__)
160
161                 callbackMapping = {
162                         "on_dialpad_quit": self._on_close,
163                 }
164                 self._widgetTree.signal_autoconnect(callbackMapping)
165
166                 self._window.connect("destroy", self._on_close)
167                 self._window.set_default_size(800, 300)
168                 self._window.show_all()
169
170                 self._loginSink = gtk_toolbox.threaded_stage(
171                         gtk_toolbox.comap(
172                                 self.attempt_login,
173                                 gtk_toolbox.null_sink(),
174                         )
175                 )
176
177                 backgroundSetup = threading.Thread(target=self._idle_setup)
178                 backgroundSetup.setDaemon(True)
179                 backgroundSetup.start()
180
181         def _idle_setup(self):
182                 """
183                 If something can be done after the UI loads, push it here so it's not blocking the UI
184                 """
185                 try:
186                         # Barebones UI handlers
187                         import null_backend
188                         import null_views
189
190                         self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
191                         with gtk_toolbox.gtk_lock():
192                                 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
193                                 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
194                                 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
195                                 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
196                                 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
197
198                                 self._dialpads[self._selectedBackendId].enable()
199                                 self._accountViews[self._selectedBackendId].enable()
200                                 self._recentViews[self._selectedBackendId].enable()
201                                 self._messagesViews[self._selectedBackendId].enable()
202                                 self._contactsViews[self._selectedBackendId].enable()
203
204                         # Setup maemo specifics
205                         try:
206                                 import osso
207                         except ImportError:
208                                 osso = None
209                         self._osso = None
210                         if osso is not None:
211                                 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
212                                 device = osso.DeviceState(self._osso)
213                                 device.set_device_state_callback(self._on_device_state_change, 0)
214                         else:
215                                 pass # warnings.warn("No OSSO", UserWarning, 2)
216
217                         try:
218                                 import alarm_handler
219                                 self._alarmHandler = alarm_handler.AlarmHandler()
220                         except ImportError:
221                                 alarm_handler = None
222                         except Exception:
223                                 with gtk_toolbox.gtk_lock():
224                                         self._errorDisplay.push_exception()
225                                 alarm_handler = None
226                         if hildon is not None:
227                                 import led_handler
228                                 self._ledHandler = led_handler.LedHandler()
229
230                         # Setup maemo specifics
231                         try:
232                                 import conic
233                         except ImportError:
234                                 conic = None
235                         self._connection = None
236                         if conic is not None:
237                                 self._connection = conic.Connection()
238                                 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
239                                 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
240                         else:
241                                 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
242
243                         # Setup costly backends
244                         import gv_backend
245                         import gc_backend
246                         import file_backend
247                         import evo_backend
248                         import gc_views
249
250                         try:
251                                 os.makedirs(constants._data_path_)
252                         except OSError, e:
253                                 if e.errno != 17:
254                                         raise
255                         gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
256                         gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
257                         self._defaultBackendId = self._guess_preferred_backend((
258                                 (self.GC_BACKEND, gcCookiePath),
259                                 (self.GV_BACKEND, gvCookiePath),
260                         ))
261
262                         self._phoneBackends.update({
263                                 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
264                                 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
265                         })
266                         with gtk_toolbox.gtk_lock():
267                                 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
268                                 unifiedDialpad.set_number("")
269                                 self._dialpads.update({
270                                         self.GC_BACKEND: unifiedDialpad,
271                                         self.GV_BACKEND: unifiedDialpad,
272                                 })
273                                 self._accountViews.update({
274                                         self.GC_BACKEND: gc_views.AccountInfo(
275                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
276                                         ),
277                                         self.GV_BACKEND: gc_views.AccountInfo(
278                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
279                                         ),
280                                 })
281                                 self._accountViews[self.GC_BACKEND].save_everything = lambda *args: None
282                                 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
283                                 self._recentViews.update({
284                                         self.GC_BACKEND: gc_views.RecentCallsView(
285                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
286                                         ),
287                                         self.GV_BACKEND: gc_views.RecentCallsView(
288                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
289                                         ),
290                                 })
291                                 self._messagesViews.update({
292                                         self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
293                                         self.GV_BACKEND: gc_views.MessagesView(
294                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
295                                         ),
296                                 })
297                                 self._contactsViews.update({
298                                         self.GC_BACKEND: gc_views.ContactsView(
299                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
300                                         ),
301                                         self.GV_BACKEND: gc_views.ContactsView(
302                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
303                                         ),
304                                 })
305
306                         evoBackend = evo_backend.EvolutionAddressBook()
307                         fsContactsPath = os.path.join(constants._data_path_, "contacts")
308                         fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
309                         for backendId in (self.GV_BACKEND, self.GC_BACKEND):
310                                 self._dialpads[backendId].number_selected = self._select_action
311                                 self._recentViews[backendId].number_selected = self._select_action
312                                 self._messagesViews[backendId].number_selected = self._select_action
313                                 self._contactsViews[backendId].number_selected = self._select_action
314
315                                 addressBooks = [
316                                         self._phoneBackends[backendId],
317                                         evoBackend,
318                                         fileBackend,
319                                 ]
320                                 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
321                                 self._contactsViews[backendId].append(mergedBook)
322                                 self._contactsViews[backendId].extend(addressBooks)
323                                 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
324
325                         callbackMapping = {
326                                 "on_paste": self._on_paste,
327                                 "on_refresh": self._on_menu_refresh,
328                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
329                                 "on_notebook_switch_page": self._on_notebook_switch_page,
330                                 "on_about_activate": self._on_about_activate,
331                         }
332                         self._widgetTree.signal_autoconnect(callbackMapping)
333
334                         with gtk_toolbox.gtk_lock():
335                                 self._originalCurrentLabels = [
336                                         self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
337                                         for pageIndex in xrange(self._notebook.get_n_pages())
338                                 ]
339                                 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
340                                 self._notebookTapHandler.enable()
341                         self._notebookTapHandler.on_tap = self._reset_tab_refresh
342                         self._notebookTapHandler.on_hold = self._on_tab_refresh
343                         self._notebookTapHandler.on_holding = self._set_tab_refresh
344                         self._notebookTapHandler.on_cancel = self._reset_tab_refresh
345
346                         self._initDone = True
347
348                         config = ConfigParser.SafeConfigParser()
349                         config.read(constants._user_settings_)
350                         with gtk_toolbox.gtk_lock():
351                                 self.load_settings(config)
352
353                         self._spawn_attempt_login(2)
354                 except Exception, e:
355                         with gtk_toolbox.gtk_lock():
356                                 self._errorDisplay.push_exception()
357
358         def attempt_login(self, numOfAttempts = 10, force = False):
359                 """
360                 @todo Handle user notification better like attempting to login and failed login
361
362                 @note This must be run outside of the UI lock
363                 """
364                 try:
365                         assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
366                         assert self._initDone, "Attempting login before app is fully loaded"
367
368                         serviceId = self.NULL_BACKEND
369                         loggedIn = False
370                         if not force:
371                                 try:
372                                         self.refresh_session()
373                                         serviceId = self._defaultBackendId
374                                         loggedIn = True
375                                 except StandardError, e:
376                                         warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
377
378                         if not loggedIn:
379                                 loggedIn, serviceId = self._login_by_user(numOfAttempts)
380
381                         with gtk_toolbox.gtk_lock():
382                                 self._change_loggedin_status(serviceId)
383                 except StandardError, e:
384                         with gtk_toolbox.gtk_lock():
385                                 self._errorDisplay.push_exception()
386
387         def _spawn_attempt_login(self, *args):
388                 self._loginSink.send(args)
389
390         def refresh_session(self):
391                 """
392                 @note Thread agnostic
393                 """
394                 assert self._initDone, "Attempting login before app is fully loaded"
395
396                 loggedIn = False
397                 if not loggedIn:
398                         loggedIn = self._login_by_cookie()
399                 if not loggedIn:
400                         loggedIn = self._login_by_settings()
401
402                 if not loggedIn:
403                         raise RuntimeError("Login Failed")
404
405         def _login_by_cookie(self):
406                 """
407                 @note Thread agnostic
408                 """
409                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
410                 if loggedIn:
411                         warnings.warn(
412                                 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
413                                 UserWarning, 2
414                         )
415                 return loggedIn
416
417         def _login_by_settings(self):
418                 """
419                 @note Thread agnostic
420                 """
421                 username, password = self._credentials
422                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
423                 if loggedIn:
424                         self._credentials = username, password
425                         warnings.warn(
426                                 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
427                                 UserWarning, 2
428                         )
429                 return loggedIn
430
431         def _login_by_user(self, numOfAttempts):
432                 """
433                 @note This must be run outside of the UI lock
434                 """
435                 loggedIn, (username, password) = False, self._credentials
436                 tmpServiceId = self.NULL_BACKEND
437                 for attemptCount in xrange(numOfAttempts):
438                         if loggedIn:
439                                 break
440                         availableServices = (
441                                 (self.GV_BACKEND, "Google Voice"),
442                                 (self.GC_BACKEND, "Grand Central"),
443                         )
444                         with gtk_toolbox.gtk_lock():
445                                 credentials = self._credentialsDialog.request_credentials_from(
446                                         availableServices, defaultCredentials = self._credentials
447                                 )
448                         tmpServiceId, username, password = credentials
449                         loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
450
451                 if loggedIn:
452                         serviceId = tmpServiceId
453                         self._credentials = username, password
454                         warnings.warn(
455                                 "Logged into %r through user request" % self._phoneBackends[serviceId],
456                                 UserWarning, 2
457                         )
458                 else:
459                         serviceId = self.NULL_BACKEND
460
461                 return loggedIn, serviceId
462
463         def _select_action(self, action, number, message):
464                 self.refresh_session()
465                 if action == "select":
466                         self._dialpads[self._selectedBackendId].set_number(number)
467                         self._notebook.set_current_page(self.KEYPAD_TAB)
468                 elif action == "dial":
469                         self._on_dial_clicked(number)
470                 elif action == "sms":
471                         self._on_sms_clicked(number, message)
472                 else:
473                         assert False, "Unknown action: %s" % action
474
475         def _change_loggedin_status(self, newStatus):
476                 oldStatus = self._selectedBackendId
477                 if oldStatus == newStatus:
478                         return
479
480                 self._dialpads[oldStatus].disable()
481                 self._accountViews[oldStatus].disable()
482                 self._recentViews[oldStatus].disable()
483                 self._messagesViews[oldStatus].disable()
484                 self._contactsViews[oldStatus].disable()
485
486                 self._dialpads[newStatus].enable()
487                 self._accountViews[newStatus].enable()
488                 self._recentViews[newStatus].enable()
489                 self._messagesViews[newStatus].enable()
490                 self._contactsViews[newStatus].enable()
491
492                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
493                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
494                 self._accountViews[self._selectedBackendId].update()
495
496                 self._selectedBackendId = newStatus
497
498         def load_settings(self, config):
499                 """
500                 @note UI Thread
501                 """
502                 try:
503                         self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
504                         blobs = (
505                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
506                                 for i in xrange(len(self._credentials))
507                         )
508                         creds = (
509                                 base64.b64decode(blob)
510                                 for blob in blobs
511                         )
512                         self._credentials = tuple(creds)
513
514                         if self._alarmHandler is not None:
515                                 self._alarmHandler.load_settings(config, "alarm")
516                 except ConfigParser.NoOptionError, e:
517                         warnings.warn(
518                                 "Settings file %s is missing section %s" % (
519                                         constants._user_settings_,
520                                         e.section,
521                                 ),
522                                 stacklevel=2
523                         )
524                 except ConfigParser.NoSectionError, e:
525                         warnings.warn(
526                                 "Settings file %s is missing section %s" % (
527                                         constants._user_settings_,
528                                         e.section,
529                                 ),
530                                 stacklevel=2
531                         )
532
533                 for backendId, view in itertools.chain(
534                         self._dialpads.iteritems(),
535                         self._accountViews.iteritems(),
536                         self._messagesViews.iteritems(),
537                         self._recentViews.iteritems(),
538                         self._contactsViews.iteritems(),
539                 ):
540                         sectionName = "%s - %s" % (backendId, view.name())
541                         try:
542                                 view.load_settings(config, sectionName)
543                         except ConfigParser.NoOptionError, e:
544                                 warnings.warn(
545                                         "Settings file %s is missing section %s" % (
546                                                 constants._user_settings_,
547                                                 e.section,
548                                         ),
549                                         stacklevel=2
550                                 )
551                         except ConfigParser.NoSectionError, e:
552                                 warnings.warn(
553                                         "Settings file %s is missing section %s" % (
554                                                 constants._user_settings_,
555                                                 e.section,
556                                         ),
557                                         stacklevel=2
558                                 )
559
560         def save_settings(self, config):
561                 """
562                 @note Thread Agnostic
563                 """
564                 config.add_section(constants.__pretty_app_name__)
565                 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
566                 for i, value in enumerate(self._credentials):
567                         blob = base64.b64encode(value)
568                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
569                 config.add_section("alarm")
570                 if self._alarmHandler is not None:
571                         self._alarmHandler.save_settings(config, "alarm")
572
573                 for backendId, view in itertools.chain(
574                         self._dialpads.iteritems(),
575                         self._accountViews.iteritems(),
576                         self._messagesViews.iteritems(),
577                         self._recentViews.iteritems(),
578                         self._contactsViews.iteritems(),
579                 ):
580                         sectionName = "%s - %s" % (backendId, view.name())
581                         config.add_section(sectionName)
582                         view.save_settings(config, sectionName)
583
584         def _guess_preferred_backend(self, backendAndCookiePaths):
585                 modTimeAndPath = [
586                         (getmtime_nothrow(path), backendId, path)
587                         for backendId, path in backendAndCookiePaths
588                 ]
589                 modTimeAndPath.sort()
590                 return modTimeAndPath[-1][1]
591
592         def _save_settings(self):
593                 """
594                 @note Thread Agnostic
595                 """
596                 config = ConfigParser.SafeConfigParser()
597                 self.save_settings(config)
598                 with open(constants._user_settings_, "wb") as configFile:
599                         config.write(configFile)
600
601         def _refresh_active_tab(self):
602                 pageIndex = self._notebook.get_current_page()
603                 if pageIndex == self.CONTACTS_TAB:
604                         self._contactsViews[self._selectedBackendId].update(force=True)
605                 elif pageIndex == self.RECENT_TAB:
606                         self._recentViews[self._selectedBackendId].update(force=True)
607                 elif pageIndex == self.MESSAGES_TAB:
608                         self._messagesViews[self._selectedBackendId].update(force=True)
609
610                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
611                         if self._ledHandler is not None:
612                                 self._ledHandler.off()
613
614         def _on_close(self, *args, **kwds):
615                 try:
616                         if self._osso is not None:
617                                 self._osso.close()
618
619                         if self._initDone:
620                                 self._save_settings()
621                 finally:
622                         gtk.main_quit()
623
624         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
625                 """
626                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
627                 For system_inactivity, we have no background tasks to pause
628
629                 @note Hildon specific
630                 """
631                 if memory_low:
632                         for backendId in self.BACKENDS:
633                                 self._phoneBackends[backendId].clear_caches()
634                         self._contactsViews[self._selectedBackendId].clear_caches()
635                         gc.collect()
636
637                 if save_unsaved_data or shutdown:
638                         self._save_settings()
639
640         def _on_connection_change(self, connection, event, magicIdentifier):
641                 """
642                 @note Hildon specific
643                 """
644                 import conic
645
646                 status = event.get_status()
647                 error = event.get_error()
648                 iap_id = event.get_iap_id()
649                 bearer = event.get_bearer_type()
650
651                 if status == conic.STATUS_CONNECTED:
652                         if self._initDone:
653                                 self._spawn_attempt_login(2)
654                 elif status == conic.STATUS_DISCONNECTED:
655                         if self._initDone:
656                                 self._defaultBackendId = self._selectedBackendId
657                                 self._change_loggedin_status(self.NULL_BACKEND)
658
659         def _on_window_state_change(self, widget, event, *args):
660                 """
661                 @note Hildon specific
662                 """
663                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
664                         self._isFullScreen = True
665                 else:
666                         self._isFullScreen = False
667
668         def _on_key_press(self, widget, event, *args):
669                 """
670                 @note Hildon specific
671                 """
672                 if event.keyval == gtk.keysyms.F6:
673                         if self._isFullScreen:
674                                 self._window.unfullscreen()
675                         else:
676                                 self._window.fullscreen()
677
678         def _on_clearcookies_clicked(self, *args):
679                 self._phoneBackends[self._selectedBackendId].logout()
680                 self._accountViews[self._selectedBackendId].clear()
681                 self._recentViews[self._selectedBackendId].clear()
682                 self._messagesViews[self._selectedBackendId].clear()
683                 self._contactsViews[self._selectedBackendId].clear()
684                 self._change_loggedin_status(self.NULL_BACKEND)
685
686                 self._spawn_attempt_login(2, True)
687
688         def _on_notebook_switch_page(self, notebook, page, pageIndex):
689                 self._reset_tab_refresh()
690
691                 didRecentUpdate = False
692                 didMessagesUpdate = False
693
694                 if pageIndex == self.RECENT_TAB:
695                         didRecentUpdate = self._recentViews[self._selectedBackendId].update()
696                 elif pageIndex == self.MESSAGES_TAB:
697                         didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
698                 elif pageIndex == self.CONTACTS_TAB:
699                         self._contactsViews[self._selectedBackendId].update()
700                 elif pageIndex == self.ACCOUNT_TAB:
701                         self._accountViews[self._selectedBackendId].update()
702
703                 if didRecentUpdate or didMessagesUpdate:
704                         if self._ledHandler is not None:
705                                 self._ledHandler.off()
706
707         def _set_tab_refresh(self, *args):
708                 pageIndex = self._notebook.get_current_page()
709                 child = self._notebook.get_nth_page(pageIndex)
710                 self._notebook.get_tab_label(child).set_text("Refresh?")
711                 return False
712
713         def _reset_tab_refresh(self, *args):
714                 pageIndex = self._notebook.get_current_page()
715                 child = self._notebook.get_nth_page(pageIndex)
716                 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
717                 return False
718
719         def _on_tab_refresh(self, *args):
720                 self._refresh_active_tab()
721                 self._reset_tab_refresh()
722                 return False
723
724         def _on_sms_clicked(self, number, message):
725                 assert number, "No number specified"
726                 assert message, "Empty message"
727                 try:
728                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
729                 except StandardError, e:
730                         loggedIn = False
731                         self._errorDisplay.push_exception()
732                         return
733
734                 if not loggedIn:
735                         self._errorDisplay.push_message(
736                                 "Backend link with grandcentral is not working, please try again"
737                         )
738                         return
739
740                 dialed = False
741                 try:
742                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
743                         dialed = True
744                 except StandardError, e:
745                         self._errorDisplay.push_exception()
746                 except ValueError, e:
747                         self._errorDisplay.push_exception()
748
749                 if dialed:
750                         self._dialpads[self._selectedBackendId].clear()
751
752         def _on_dial_clicked(self, number):
753                 assert number, "No number to call"
754                 try:
755                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
756                 except StandardError, e:
757                         loggedIn = False
758                         self._errorDisplay.push_exception()
759                         return
760
761                 if not loggedIn:
762                         self._errorDisplay.push_message(
763                                 "Backend link with grandcentral is not working, please try again"
764                         )
765                         return
766
767                 dialed = False
768                 try:
769                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
770                         self._phoneBackends[self._selectedBackendId].dial(number)
771                         dialed = True
772                 except StandardError, e:
773                         self._errorDisplay.push_exception()
774                 except ValueError, e:
775                         self._errorDisplay.push_exception()
776
777                 if dialed:
778                         self._dialpads[self._selectedBackendId].clear()
779
780         def _on_menu_refresh(self, *args):
781                 self._refresh_active_tab()
782
783         def _on_paste(self, *args):
784                 contents = self._clipboard.wait_for_text()
785                 self._dialpads[self._selectedBackendId].set_number(contents)
786
787         def _on_about_activate(self, *args):
788                 dlg = gtk.AboutDialog()
789                 dlg.set_name(constants.__pretty_app_name__)
790                 dlg.set_version(constants.__version__)
791                 dlg.set_copyright("Copyright 2008 - LGPL")
792                 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice/Grandcentral account.  This application is not affiliated with Google in any way")
793                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
794                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
795                 dlg.run()
796                 dlg.destroy()
797
798
799 def run_doctest():
800         import doctest
801
802         failureCount, testCount = doctest.testmod()
803         if not failureCount:
804                 print "Tests Successful"
805                 sys.exit(0)
806         else:
807                 sys.exit(1)
808
809
810 def run_dialpad():
811         _lock_file = os.path.join(constants._data_path_, ".lock")
812         #with gtk_toolbox.flock(_lock_file, 0):
813         gtk.gdk.threads_init()
814
815         if hildon is not None:
816                 gtk.set_application_name(constants.__pretty_app_name__)
817         handle = Dialcentral()
818         gtk.main()
819
820
821 class DummyOptions(object):
822
823         def __init__(self):
824                 self.test = False
825
826
827 if __name__ == "__main__":
828         if len(sys.argv) > 1:
829                 try:
830                         import optparse
831                 except ImportError:
832                         optparse = None
833
834                 if optparse is not None:
835                         parser = optparse.OptionParser()
836                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
837                         (commandOptions, commandArgs) = parser.parse_args()
838         else:
839                 commandOptions = DummyOptions()
840                 commandArgs = []
841
842         if commandOptions.test:
843                 run_doctest()
844         else:
845                 run_dialpad()