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