Fixing the phone type selector
[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 @todo Add logging support to make debugging issues for people a lot easier
22 """
23
24
25 from __future__ import with_statement
26
27 import sys
28 import gc
29 import os
30 import threading
31 import warnings
32 import traceback
33
34 import gtk
35 import gtk.glade
36
37 try:
38         import hildon
39 except ImportError:
40         hildon = None
41
42 import gtk_toolbox
43
44
45 def getmtime_nothrow(path):
46         try:
47                 return os.path.getmtime(path)
48         except StandardError:
49                 return 0
50
51
52 class Dialcentral(object):
53
54         __pretty_app_name__ = "DialCentral"
55         __app_name__ = "dialcentral"
56         __version__ = "0.9.6"
57         __app_magic__ = 0xdeadbeef
58
59         _glade_files = [
60                 '/usr/lib/dialcentral/dialcentral.glade',
61                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
62                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
63         ]
64
65         KEYPAD_TAB = 0
66         RECENT_TAB = 1
67         MESSAGES_TAB = 2
68         CONTACTS_TAB = 3
69         ACCOUNT_TAB = 4
70
71         NULL_BACKEND = 0
72         GC_BACKEND = 1
73         GV_BACKEND = 2
74         BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
75
76         _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
77
78         def __init__(self):
79                 self._connection = None
80                 self._osso = None
81                 self._clipboard = gtk.clipboard_get()
82
83                 self._deviceIsOnline = True
84                 self._selectedBackendId = self.NULL_BACKEND
85                 self._defaultBackendId = self.GC_BACKEND
86                 self._phoneBackends = None
87                 self._dialpads = None
88                 self._accountViews = None
89                 self._messagesViews = None
90                 self._recentViews = None
91                 self._contactsViews = None
92
93                 for path in self._glade_files:
94                         if os.path.isfile(path):
95                                 self._widgetTree = gtk.glade.XML(path)
96                                 break
97                 else:
98                         self.display_error_message("Cannot find dialcentral.glade")
99                         gtk.main_quit()
100                         return
101
102                 self._window = self._widgetTree.get_widget("mainWindow")
103                 self._notebook = self._widgetTree.get_widget("notebook")
104                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
105                 self._credentials = gtk_toolbox.LoginWindow(self._widgetTree)
106
107                 self._app = None
108                 self._isFullScreen = False
109                 if hildon is not None:
110                         self._app = hildon.Program()
111                         self._window = hildon.Window()
112                         self._widgetTree.get_widget("vbox1").reparent(self._window)
113                         self._app.add_window(self._window)
114                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
115                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
116                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
117                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
118                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
119
120                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
121                         menu = gtk.Menu()
122                         for child in gtkMenu.get_children():
123                                 child.reparent(menu)
124                         self._window.set_menu(menu)
125                         gtkMenu.destroy()
126
127                         self._window.connect("key-press-event", self._on_key_press)
128                         self._window.connect("window-state-event", self._on_window_state_change)
129                 else:
130                         pass # warnings.warn("No Hildon", UserWarning, 2)
131
132                 if hildon is not None:
133                         self._window.set_title("Keypad")
134                 else:
135                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
136
137                 callbackMapping = {
138                         "on_dialpad_quit": self._on_close,
139                 }
140                 self._widgetTree.signal_autoconnect(callbackMapping)
141
142                 if self._window:
143                         self._window.connect("destroy", gtk.main_quit)
144                         self._window.show_all()
145                         self._window.set_default_size(800, 300)
146
147                 backgroundSetup = threading.Thread(target=self._idle_setup)
148                 backgroundSetup.setDaemon(True)
149                 backgroundSetup.start()
150
151         def _idle_setup(self):
152                 """
153                 If something can be done after the UI loads, push it here so it's not blocking the UI
154                 """
155                 # Barebones UI handlers
156                 import null_backend
157                 import null_views
158
159                 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
160                 with gtk_toolbox.gtk_lock():
161                         self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
162                         self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
163                         self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
164                         self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
165                         self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
166
167                         self._dialpads[self._selectedBackendId].enable()
168                         self._accountViews[self._selectedBackendId].enable()
169                         self._recentViews[self._selectedBackendId].enable()
170                         self._messagesViews[self._selectedBackendId].enable()
171                         self._contactsViews[self._selectedBackendId].enable()
172
173                 # Setup maemo specifics
174                 try:
175                         import osso
176                 except ImportError:
177                         osso = None
178                 self._osso = None
179                 if osso is not None:
180                         self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
181                         device = osso.DeviceState(self._osso)
182                         device.set_device_state_callback(self._on_device_state_change, 0)
183                 else:
184                         pass # warnings.warn("No OSSO", UserWarning)
185
186                 try:
187                         import conic
188                 except ImportError:
189                         conic = None
190                 self._connection = None
191                 if conic is not None:
192                         self._connection = conic.Connection()
193                         self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
194                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
195                 else:
196                         pass # warnings.warn("No Internet Connectivity API ", UserWarning)
197
198                 # Setup costly backends
199                 import gv_backend
200                 import gc_backend
201                 import file_backend
202                 import evo_backend
203                 import gc_views
204
205                 try:
206                         os.makedirs(self._data_path)
207                 except OSError, e:
208                         if e.errno != 17:
209                                 raise
210                 gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
211                 gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
212                 self._defaultBackendId = self._guess_preferred_backend((
213                         (self.GC_BACKEND, gcCookiePath),
214                         (self.GV_BACKEND, gvCookiePath),
215                 ))
216
217                 self._phoneBackends.update({
218                         self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
219                         self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
220                 })
221                 with gtk_toolbox.gtk_lock():
222                         unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
223                         unifiedDialpad.set_number("")
224                         self._dialpads.update({
225                                 self.GC_BACKEND: unifiedDialpad,
226                                 self.GV_BACKEND: unifiedDialpad,
227                         })
228                         self._accountViews.update({
229                                 self.GC_BACKEND: gc_views.AccountInfo(
230                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
231                                 ),
232                                 self.GV_BACKEND: gc_views.AccountInfo(
233                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
234                                 ),
235                         })
236                         self._recentViews.update({
237                                 self.GC_BACKEND: gc_views.RecentCallsView(
238                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
239                                 ),
240                                 self.GV_BACKEND: gc_views.RecentCallsView(
241                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
242                                 ),
243                         })
244                         self._messagesViews.update({
245                                 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
246                                 self.GV_BACKEND: gc_views.MessagesView(
247                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
248                                 ),
249                         })
250                         self._contactsViews.update({
251                                 self.GC_BACKEND: gc_views.ContactsView(
252                                         self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
253                                 ),
254                                 self.GV_BACKEND: gc_views.ContactsView(
255                                         self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
256                                 ),
257                         })
258
259                 evoBackend = evo_backend.EvolutionAddressBook()
260                 fsContactsPath = os.path.join(self._data_path, "contacts")
261                 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
262                 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
263                         self._dialpads[backendId].dial = self._on_dial_clicked
264                         self._recentViews[backendId].number_selected = self._on_number_selected
265                         self._contactsViews[backendId].number_selected = self._on_number_selected
266
267                         addressBooks = [
268                                 self._phoneBackends[backendId],
269                                 evoBackend,
270                                 fileBackend,
271                         ]
272                         mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
273                         self._contactsViews[backendId].append(mergedBook)
274                         self._contactsViews[backendId].extend(addressBooks)
275                         self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
276
277                 callbackMapping = {
278                         "on_paste": self._on_paste,
279                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
280                         "on_notebook_switch_page": self._on_notebook_switch_page,
281                         "on_about_activate": self._on_about_activate,
282                 }
283                 self._widgetTree.signal_autoconnect(callbackMapping)
284
285                 self.attempt_login(2)
286
287                 return False
288
289         def attempt_login(self, numOfAttempts = 10):
290                 """
291                 @todo Handle user notification better like attempting to login and failed login
292
293                 @note Not meant to be called directly, but run as a seperate thread.
294                 """
295                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
296
297                 if not self._deviceIsOnline:
298                         warnings.warn("Attempted to login while device was offline")
299                         return False
300                 elif self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
301                         warnings.warn(
302                                 "Attempted to login before initialization is complete, did an event fire early?"
303                         )
304                         return False
305
306                 loggedIn = False
307                 try:
308                         if self._phoneBackends[self._defaultBackendId].is_authed():
309                                 serviceId = self._defaultBackendId
310                                 loggedIn = True
311                         for x in xrange(numOfAttempts):
312                                 if loggedIn:
313                                         break
314                                 with gtk_toolbox.gtk_lock():
315                                         availableServices = {
316                                                 self.GV_BACKEND: "Google Voice",
317                                                 self.GC_BACKEND: "Grand Central",
318                                         }
319                                         credentials = self._credentials.request_credentials_from(availableServices)
320                                         serviceId, username, password = credentials
321
322                                 loggedIn = self._phoneBackends[serviceId].login(username, password)
323                 except RuntimeError, e:
324                         warnings.warn(traceback.format_exc())
325                         self._errorDisplay.push_exception_with_lock(e)
326
327                 with gtk_toolbox.gtk_lock():
328                         if not loggedIn:
329                                 self._errorDisplay.push_message("Login Failed")
330                         self._change_loggedin_status(serviceId if loggedIn else self.NULL_BACKEND)
331                 return loggedIn
332
333         def display_error_message(self, msg):
334                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
335
336                 def close(dialog, response, editor):
337                         editor.about_dialog = None
338                         dialog.destroy()
339                 error_dialog.connect("response", close, self)
340                 error_dialog.run()
341
342         def _on_close(self, *args, **kwds):
343                 if self._osso is not None:
344                         self._osso.close()
345                 gtk.main_quit()
346
347         def _change_loggedin_status(self, newStatus):
348                 oldStatus = self._selectedBackendId
349                 if oldStatus == newStatus:
350                         return
351
352                 self._dialpads[oldStatus].disable()
353                 self._accountViews[oldStatus].disable()
354                 self._recentViews[oldStatus].disable()
355                 self._messagesViews[oldStatus].disable()
356                 self._contactsViews[oldStatus].disable()
357
358                 self._dialpads[newStatus].enable()
359                 self._accountViews[newStatus].enable()
360                 self._recentViews[newStatus].enable()
361                 self._messagesViews[newStatus].enable()
362                 self._contactsViews[newStatus].enable()
363
364                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
365                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
366                 self._accountViews[self._selectedBackendId].update()
367
368                 self._selectedBackendId = newStatus
369
370         def _guess_preferred_backend(self, backendAndCookiePaths):
371                 modTimeAndPath = [
372                         (getmtime_nothrow(path), backendId, path)
373                         for backendId, path in backendAndCookiePaths
374                 ]
375                 modTimeAndPath.sort()
376                 return modTimeAndPath[-1][1]
377
378         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
379                 """
380                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
381                 For system_inactivity, we have no background tasks to pause
382
383                 @note Hildon specific
384                 """
385                 if memory_low:
386                         for backendId in self.BACKENDS:
387                                 self._phoneBackends[backendId].clear_caches()
388                         self._contactsViews[self._selectedBackendId].clear_caches()
389                         gc.collect()
390
391         def _on_connection_change(self, connection, event, magicIdentifier):
392                 """
393                 @note Hildon specific
394                 """
395                 import conic
396
397                 status = event.get_status()
398                 error = event.get_error()
399                 iap_id = event.get_iap_id()
400                 bearer = event.get_bearer_type()
401
402                 if status == conic.STATUS_CONNECTED:
403                         self._deviceIsOnline = True
404                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
405                         backgroundLogin.setDaemon(True)
406                         backgroundLogin.start()
407                 elif status == conic.STATUS_DISCONNECTED:
408                         self._deviceIsOnline = False
409                         self._defaultBackendId = self._selectedBackendId
410                         self._change_loggedin_status(self.NULL_BACKEND)
411
412         def _on_window_state_change(self, widget, event, *args):
413                 """
414                 @note Hildon specific
415                 """
416                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
417                         self._isFullScreen = True
418                 else:
419                         self._isFullScreen = False
420
421         def _on_key_press(self, widget, event, *args):
422                 """
423                 @note Hildon specific
424                 """
425                 if event.keyval == gtk.keysyms.F6:
426                         if self._isFullScreen:
427                                 self._window.unfullscreen()
428                         else:
429                                 self._window.fullscreen()
430
431         def _on_clearcookies_clicked(self, *args):
432                 self._phoneBackends[self._selectedBackendId].logout()
433                 self._accountViews[self._selectedBackendId].clear()
434                 self._recentViews[self._selectedBackendId].clear()
435                 self._messagesViews[self._selectedBackendId].clear()
436                 self._contactsViews[self._selectedBackendId].clear()
437                 self._change_loggedin_status(self.NULL_BACKEND)
438
439                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
440                 backgroundLogin.setDaemon(True)
441                 backgroundLogin.start()
442
443         def _on_notebook_switch_page(self, notebook, page, page_num):
444                 if page_num == self.CONTACTS_TAB:
445                         self._contactsViews[self._selectedBackendId].update()
446                 elif page_num == self.RECENT_TAB:
447                         self._recentViews[self._selectedBackendId].update()
448                 elif page_num == self.MESSAGES_TAB:
449                         self._messagesViews[self._selectedBackendId].update()
450
451                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
452                 if hildon is not None:
453                         self._window.set_title(tabTitle)
454                 else:
455                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
456
457         def _on_number_selected(self, action, number, message):
458                 if action == "select":
459                         self._dialpads[self._selectedBackendId].set_number(number)
460                         self._notebook.set_current_page(0)
461                 elif action == "dial":
462                         self._on_dial_clicked(number)
463                 elif action == "sms":
464                         self._on_sms_clicked(number, message)
465                 else:
466                         assert False, "Unknown action: %s" % action
467
468         def _on_sms_clicked(self, number, message):
469                 """
470                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
471                 """
472                 assert number
473                 assert message
474                 try:
475                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
476                 except RuntimeError, e:
477                         loggedIn = False
478                         self._errorDisplay.push_exception(e)
479                         return
480
481                 if not loggedIn:
482                         self._errorDisplay.push_message(
483                                 "Backend link with grandcentral is not working, please try again"
484                         )
485                         return
486
487                 dialed = False
488                 try:
489                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
490                         dialed = True
491                 except RuntimeError, e:
492                         self._errorDisplay.push_exception(e)
493                 except ValueError, e:
494                         self._errorDisplay.push_exception(e)
495
496         def _on_dial_clicked(self, number):
497                 """
498                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
499                 """
500                 assert number
501                 try:
502                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
503                 except RuntimeError, e:
504                         loggedIn = False
505                         self._errorDisplay.push_exception(e)
506                         return
507
508                 if not loggedIn:
509                         self._errorDisplay.push_message(
510                                 "Backend link with grandcentral is not working, please try again"
511                         )
512                         return
513
514                 dialed = False
515                 try:
516                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
517                         self._phoneBackends[self._selectedBackendId].dial(number)
518                         dialed = True
519                 except RuntimeError, e:
520                         self._errorDisplay.push_exception(e)
521                 except ValueError, e:
522                         self._errorDisplay.push_exception(e)
523
524                 if dialed:
525                         self._dialpads[self._selectedBackendId].clear()
526                         self._recentViews[self._selectedBackendId].clear()
527
528         def _on_paste(self, *args):
529                 contents = self._clipboard.wait_for_text()
530                 self._dialpads[self._selectedBackendId].set_number(contents)
531
532         def _on_about_activate(self, *args):
533                 dlg = gtk.AboutDialog()
534                 dlg.set_name(self.__pretty_app_name__)
535                 dlg.set_version(self.__version__)
536                 dlg.set_copyright("Copyright 2008 - LGPL")
537                 dlg.set_comments("Dialer is designed to interface with your Google Grandcentral account.  This application is not affiliated with Google or Grandcentral in any way")
538                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
539                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
540                 dlg.run()
541                 dlg.destroy()
542
543
544 def run_doctest():
545         import doctest
546
547         failureCount, testCount = doctest.testmod()
548         if not failureCount:
549                 print "Tests Successful"
550                 sys.exit(0)
551         else:
552                 sys.exit(1)
553
554
555 def run_dialpad():
556         gtk.gdk.threads_init()
557         if hildon is not None:
558                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
559         handle = Dialcentral()
560         gtk.main()
561
562
563 class DummyOptions(object):
564
565         def __init__(self):
566                 self.test = False
567
568
569 if __name__ == "__main__":
570         if len(sys.argv) > 1:
571                 try:
572                         import optparse
573                 except ImportError:
574                         optparse = None
575
576                 if optparse is not None:
577                         parser = optparse.OptionParser()
578                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
579                         (commandOptions, commandArgs) = parser.parse_args()
580         else:
581                 commandOptions = DummyOptions()
582                 commandArgs = []
583
584         if commandOptions.test:
585                 run_doctest()
586         else:
587                 run_dialpad()