4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
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.
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.
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
21 @bug When switching to tab while logging in, it doesn't refresh once logged int
25 from __future__ import with_statement
44 def getmtime_nothrow(path):
46 return os.path.getmtime(path)
51 def display_error_message(msg):
52 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
54 def close(dialog, response):
56 error_dialog.connect("response", close)
60 class Dialcentral(object):
63 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
64 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
65 '/usr/lib/dialcentral/dialcentral.glade',
76 BACKENDS = (NULL_BACKEND, GV_BACKEND)
79 self._initDone = False
80 self._connection = None
82 self._clipboard = gtk.clipboard_get()
84 self._credentials = ("", "")
85 self._selectedBackendId = self.NULL_BACKEND
86 self._defaultBackendId = self.GV_BACKEND
87 self._phoneBackends = None
89 self._accountViews = None
90 self._messagesViews = None
91 self._recentViews = None
92 self._contactsViews = None
93 self._alarmHandler = None
94 self._ledHandler = None
95 self._originalCurrentLabels = []
97 for path in self._glade_files:
98 if os.path.isfile(path):
99 self._widgetTree = gtk.glade.XML(path)
102 display_error_message("Cannot find dialcentral.glade")
106 self._window = self._widgetTree.get_widget("mainWindow")
107 self._notebook = self._widgetTree.get_widget("notebook")
108 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
109 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
111 self._isFullScreen = False
112 self._app = hildonize.get_app_class()()
113 self._window = hildonize.hildonize_window(self._app, self._window)
114 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
115 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
116 hildonize.hildonize_combo_entry(self._widgetTree.get_widget("callbackcombo").get_child())
118 for scrollingWidget in (
119 'recent_scrolledwindow',
120 'message_scrolledwindow',
121 'contacts_scrolledwindow',
122 "phoneSelectionMessages_scrolledwindow",
123 "smsMessages_scrolledwindow",
125 hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget))
126 for scrollingWidget in (
127 "phonetypes_scrolledwindow",
128 "smsMessage_scrolledEntry",
130 hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget))
132 replacementButtons = [gtk.Button("Test")]
133 menu = hildonize.hildonize_menu(
135 self._widgetTree.get_widget("dialpad_menubar"),
139 if hildonize.IS_HILDON_SUPPORTED:
140 self._window.connect("key-press-event", self._on_key_press)
141 self._window.connect("window-state-event", self._on_window_state_change)
143 logging.warning("No hildonization support")
145 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
147 self._window.connect("destroy", self._on_close)
148 self._window.set_default_size(800, 300)
149 self._window.show_all()
151 self._loginSink = gtk_toolbox.threaded_stage(
154 gtk_toolbox.null_sink(),
158 backgroundSetup = threading.Thread(target=self._idle_setup)
159 backgroundSetup.setDaemon(True)
160 backgroundSetup.start()
162 def _idle_setup(self):
164 If something can be done after the UI loads, push it here so it's not blocking the UI
166 # Barebones UI handlers
171 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
172 with gtk_toolbox.gtk_lock():
173 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
174 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
175 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
176 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
177 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
179 self._dialpads[self._selectedBackendId].enable()
180 self._accountViews[self._selectedBackendId].enable()
181 self._recentViews[self._selectedBackendId].enable()
182 self._messagesViews[self._selectedBackendId].enable()
183 self._contactsViews[self._selectedBackendId].enable()
185 with gtk_toolbox.gtk_lock():
186 self._errorDisplay.push_exception()
188 # Setup maemo specifics
192 except (ImportError, OSError):
196 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
197 device = osso.DeviceState(self._osso)
198 device.set_device_state_callback(self._on_device_state_change, 0)
200 logging.warning("No device state support")
204 self._alarmHandler = alarm_handler.AlarmHandler()
205 except (ImportError, OSError):
208 with gtk_toolbox.gtk_lock():
209 self._errorDisplay.push_exception()
211 logging.warning("No notification support")
212 if hildonize.IS_HILDON_SUPPORTED:
215 self._ledHandler = led_handler.LedHandler()
217 logging.exception('LED Handling failed: "%s"' % str(e))
218 self._ledHandler = None
220 self._ledHandler = None
224 except (ImportError, OSError):
226 self._connection = None
227 if conic is not None:
228 self._connection = conic.Connection()
229 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
230 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
232 logging.warning("No connection support")
234 with gtk_toolbox.gtk_lock():
235 self._errorDisplay.push_exception()
237 # Setup costly backends
244 os.makedirs(constants._data_path_)
248 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
250 self._phoneBackends.update({
251 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
253 with gtk_toolbox.gtk_lock():
254 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
255 self._dialpads.update({
256 self.GV_BACKEND: unifiedDialpad,
258 self._accountViews.update({
259 self.GV_BACKEND: gv_views.AccountInfo(
260 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
263 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
264 self._recentViews.update({
265 self.GV_BACKEND: gv_views.RecentCallsView(
266 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
269 self._messagesViews.update({
270 self.GV_BACKEND: gv_views.MessagesView(
271 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
274 self._contactsViews.update({
275 self.GV_BACKEND: gv_views.ContactsView(
276 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
280 fsContactsPath = os.path.join(constants._data_path_, "contacts")
281 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
283 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
284 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
285 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
286 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
289 self._phoneBackends[self.GV_BACKEND],
292 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
293 self._contactsViews[self.GV_BACKEND].append(mergedBook)
294 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
295 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
298 "on_paste": self._on_paste,
299 "on_refresh": self._on_menu_refresh,
300 "on_clearcookies_clicked": self._on_clearcookies_clicked,
301 "on_about_activate": self._on_about_activate,
303 if hildonize.GTK_MENU_USED:
304 self._widgetTree.signal_autoconnect(callbackMapping)
305 self._notebook.connect("switch-page", self._on_notebook_switch_page)
306 self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
308 with gtk_toolbox.gtk_lock():
309 self._originalCurrentLabels = [
310 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
311 for pageIndex in xrange(self._notebook.get_n_pages())
313 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
314 self._notebookTapHandler.enable()
315 self._notebookTapHandler.on_tap = self._reset_tab_refresh
316 self._notebookTapHandler.on_hold = self._on_tab_refresh
317 self._notebookTapHandler.on_holding = self._set_tab_refresh
318 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
320 self._initDone = True
322 config = ConfigParser.SafeConfigParser()
323 config.read(constants._user_settings_)
324 with gtk_toolbox.gtk_lock():
325 self.load_settings(config)
327 with gtk_toolbox.gtk_lock():
328 self._errorDisplay.push_exception()
330 self._spawn_attempt_login(2)
332 def attempt_login(self, numOfAttempts = 10, force = False):
334 @todo Handle user notification better like attempting to login and failed login
336 @note This must be run outside of the UI lock
339 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
340 assert self._initDone, "Attempting login before app is fully loaded"
342 serviceId = self.NULL_BACKEND
346 self.refresh_session()
347 serviceId = self._defaultBackendId
350 logging.exception('Session refresh failed with the following message "%s"' % str(e))
353 loggedIn, serviceId = self._login_by_user(numOfAttempts)
355 with gtk_toolbox.gtk_lock():
356 self._change_loggedin_status(serviceId)
358 with gtk_toolbox.gtk_lock():
359 self._errorDisplay.push_exception()
361 def _spawn_attempt_login(self, *args):
362 self._loginSink.send(args)
364 def refresh_session(self):
366 @note Thread agnostic
368 assert self._initDone, "Attempting login before app is fully loaded"
372 loggedIn = self._login_by_cookie()
374 loggedIn = self._login_by_settings()
377 raise RuntimeError("Login Failed")
379 def _login_by_cookie(self):
381 @note Thread agnostic
383 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
385 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
388 def _login_by_settings(self):
390 @note Thread agnostic
392 username, password = self._credentials
393 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
395 self._credentials = username, password
396 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
399 def _login_by_user(self, numOfAttempts):
401 @note This must be run outside of the UI lock
403 loggedIn, (username, password) = False, self._credentials
404 tmpServiceId = self.GV_BACKEND
405 for attemptCount in xrange(numOfAttempts):
408 with gtk_toolbox.gtk_lock():
409 credentials = self._credentialsDialog.request_credentials(
410 defaultCredentials = self._credentials
412 if not self._phoneBackends[tmpServiceId].get_callback_number():
413 # subtle reminder to the users to configure things
414 self._notebook.set_current_page(self.ACCOUNT_TAB)
415 username, password = credentials
416 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
419 serviceId = tmpServiceId
420 self._credentials = username, password
421 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
423 serviceId = self.NULL_BACKEND
425 return loggedIn, serviceId
427 def _select_action(self, action, number, message):
428 self.refresh_session()
429 if action == "select":
430 self._dialpads[self._selectedBackendId].set_number(number)
431 self._notebook.set_current_page(self.KEYPAD_TAB)
432 elif action == "dial":
433 self._on_dial_clicked(number)
434 elif action == "sms":
435 self._on_sms_clicked(number, message)
437 assert False, "Unknown action: %s" % action
439 def _change_loggedin_status(self, newStatus):
440 oldStatus = self._selectedBackendId
441 if oldStatus == newStatus:
444 self._dialpads[oldStatus].disable()
445 self._accountViews[oldStatus].disable()
446 self._recentViews[oldStatus].disable()
447 self._messagesViews[oldStatus].disable()
448 self._contactsViews[oldStatus].disable()
450 self._dialpads[newStatus].enable()
451 self._accountViews[newStatus].enable()
452 self._recentViews[newStatus].enable()
453 self._messagesViews[newStatus].enable()
454 self._contactsViews[newStatus].enable()
456 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
457 self._phoneBackends[self._selectedBackendId].set_sane_callback()
458 self._accountViews[self._selectedBackendId].update()
459 self._refresh_active_tab()
461 self._selectedBackendId = newStatus
463 def load_settings(self, config):
468 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
470 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
471 for i in xrange(len(self._credentials))
474 base64.b64decode(blob)
477 self._credentials = tuple(creds)
479 if self._alarmHandler is not None:
480 self._alarmHandler.load_settings(config, "alarm")
481 except ConfigParser.NoOptionError, e:
483 "Settings file %s is missing section %s" % (
484 constants._user_settings_,
488 except ConfigParser.NoSectionError, e:
490 "Settings file %s is missing section %s" % (
491 constants._user_settings_,
496 for backendId, view in itertools.chain(
497 self._dialpads.iteritems(),
498 self._accountViews.iteritems(),
499 self._messagesViews.iteritems(),
500 self._recentViews.iteritems(),
501 self._contactsViews.iteritems(),
503 sectionName = "%s - %s" % (backendId, view.name())
505 view.load_settings(config, sectionName)
506 except ConfigParser.NoOptionError, e:
508 "Settings file %s is missing section %s" % (
509 constants._user_settings_,
513 except ConfigParser.NoSectionError, e:
515 "Settings file %s is missing section %s" % (
516 constants._user_settings_,
522 previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
523 if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
524 hildonize.window_to_landscape(self._window)
525 elif previousOrientation == gtk.ORIENTATION_VERTICAL:
526 hildonize.window_to_portrait(self._window)
527 except ConfigParser.NoOptionError, e:
529 "Settings file %s is missing section %s" % (
530 constants._user_settings_,
534 except ConfigParser.NoSectionError, e:
536 "Settings file %s is missing section %s" % (
537 constants._user_settings_,
542 def save_settings(self, config):
544 @note Thread Agnostic
546 config.add_section(constants.__pretty_app_name__)
547 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
548 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
549 for i, value in enumerate(self._credentials):
550 blob = base64.b64encode(value)
551 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
552 config.add_section("alarm")
553 if self._alarmHandler is not None:
554 self._alarmHandler.save_settings(config, "alarm")
556 for backendId, view in itertools.chain(
557 self._dialpads.iteritems(),
558 self._accountViews.iteritems(),
559 self._messagesViews.iteritems(),
560 self._recentViews.iteritems(),
561 self._contactsViews.iteritems(),
563 sectionName = "%s - %s" % (backendId, view.name())
564 config.add_section(sectionName)
565 view.save_settings(config, sectionName)
567 def _save_settings(self):
569 @note Thread Agnostic
571 config = ConfigParser.SafeConfigParser()
572 self.save_settings(config)
573 with open(constants._user_settings_, "wb") as configFile:
574 config.write(configFile)
576 def _refresh_active_tab(self):
577 pageIndex = self._notebook.get_current_page()
578 if pageIndex == self.CONTACTS_TAB:
579 self._contactsViews[self._selectedBackendId].update(force=True)
580 elif pageIndex == self.RECENT_TAB:
581 self._recentViews[self._selectedBackendId].update(force=True)
582 elif pageIndex == self.MESSAGES_TAB:
583 self._messagesViews[self._selectedBackendId].update(force=True)
585 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
586 if self._ledHandler is not None:
587 self._ledHandler.off()
589 def _on_close(self, *args, **kwds):
591 if self._osso is not None:
595 self._save_settings()
599 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
601 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
602 For system_inactivity, we have no background tasks to pause
604 @note Hildon specific
608 for backendId in self.BACKENDS:
609 self._phoneBackends[backendId].clear_caches()
610 self._contactsViews[self._selectedBackendId].clear_caches()
613 if save_unsaved_data or shutdown:
614 self._save_settings()
616 self._errorDisplay.push_exception()
618 def _on_connection_change(self, connection, event, magicIdentifier):
620 @note Hildon specific
625 status = event.get_status()
626 error = event.get_error()
627 iap_id = event.get_iap_id()
628 bearer = event.get_bearer_type()
630 if status == conic.STATUS_CONNECTED:
632 self._spawn_attempt_login(2)
633 elif status == conic.STATUS_DISCONNECTED:
635 self._defaultBackendId = self._selectedBackendId
636 self._change_loggedin_status(self.NULL_BACKEND)
638 self._errorDisplay.push_exception()
640 def _on_window_state_change(self, widget, event, *args):
642 @note Hildon specific
645 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
646 self._isFullScreen = True
648 self._isFullScreen = False
650 self._errorDisplay.push_exception()
652 def _on_key_press(self, widget, event, *args):
654 @note Hildon specific
657 if event.keyval == gtk.keysyms.F6:
658 if self._isFullScreen:
659 self._window.unfullscreen()
661 self._window.fullscreen()
663 self._errorDisplay.push_exception()
665 def _on_clearcookies_clicked(self, *args):
667 self._phoneBackends[self._selectedBackendId].logout()
668 self._accountViews[self._selectedBackendId].clear()
669 self._recentViews[self._selectedBackendId].clear()
670 self._messagesViews[self._selectedBackendId].clear()
671 self._contactsViews[self._selectedBackendId].clear()
672 self._change_loggedin_status(self.NULL_BACKEND)
674 self._spawn_attempt_login(2, True)
676 self._errorDisplay.push_exception()
678 def _on_notebook_switch_page(self, notebook, page, pageIndex):
680 self._reset_tab_refresh()
682 didRecentUpdate = False
683 didMessagesUpdate = False
685 if pageIndex == self.RECENT_TAB:
686 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
687 elif pageIndex == self.MESSAGES_TAB:
688 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
689 elif pageIndex == self.CONTACTS_TAB:
690 self._contactsViews[self._selectedBackendId].update()
691 elif pageIndex == self.ACCOUNT_TAB:
692 self._accountViews[self._selectedBackendId].update()
694 if didRecentUpdate or didMessagesUpdate:
695 if self._ledHandler is not None:
696 self._ledHandler.off()
698 self._errorDisplay.push_exception()
700 def _set_tab_refresh(self, *args):
702 pageIndex = self._notebook.get_current_page()
703 child = self._notebook.get_nth_page(pageIndex)
704 self._notebook.get_tab_label(child).set_text("Refresh?")
706 self._errorDisplay.push_exception()
709 def _reset_tab_refresh(self, *args):
711 pageIndex = self._notebook.get_current_page()
712 child = self._notebook.get_nth_page(pageIndex)
713 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
715 self._errorDisplay.push_exception()
718 def _on_tab_refresh(self, *args):
720 self._refresh_active_tab()
721 self._reset_tab_refresh()
723 self._errorDisplay.push_exception()
726 def _on_sms_clicked(self, number, message):
728 assert number, "No number specified"
729 assert message, "Empty message"
731 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
734 self._errorDisplay.push_exception()
738 self._errorDisplay.push_message(
739 "Backend link with grandcentral is not working, please try again"
745 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
748 self._errorDisplay.push_exception()
751 self._dialpads[self._selectedBackendId].clear()
753 self._errorDisplay.push_exception()
755 def _on_dial_clicked(self, number):
757 assert number, "No number to call"
759 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
762 self._errorDisplay.push_exception()
766 self._errorDisplay.push_message(
767 "Backend link with grandcentral is not working, please try again"
773 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
774 self._phoneBackends[self._selectedBackendId].dial(number)
777 self._errorDisplay.push_exception()
780 self._dialpads[self._selectedBackendId].clear()
782 self._errorDisplay.push_exception()
784 def _on_menu_refresh(self, *args):
786 self._refresh_active_tab()
788 self._errorDisplay.push_exception()
790 def _on_paste(self, *args):
792 contents = self._clipboard.wait_for_text()
793 if contents is not None:
794 self._dialpads[self._selectedBackendId].set_number(contents)
796 self._errorDisplay.push_exception()
798 def _on_about_activate(self, *args):
800 dlg = gtk.AboutDialog()
801 dlg.set_name(constants.__pretty_app_name__)
802 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
803 dlg.set_copyright("Copyright 2008 - LGPL")
804 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")
805 dlg.set_website("http://gc-dialer.garage.maemo.org/")
806 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
810 self._errorDisplay.push_exception()
816 failureCount, testCount = doctest.testmod()
818 print "Tests Successful"
825 _lock_file = os.path.join(constants._data_path_, ".lock")
826 #with gtk_toolbox.flock(_lock_file, 0):
827 gtk.gdk.threads_init()
829 if hildonize.IS_HILDON_SUPPORTED:
830 gtk.set_application_name(constants.__pretty_app_name__)
831 handle = Dialcentral()
835 class DummyOptions(object):
841 if __name__ == "__main__":
842 logging.basicConfig(level=logging.DEBUG)
844 if len(sys.argv) > 1:
850 if optparse is not None:
851 parser = optparse.OptionParser()
852 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
853 (commandOptions, commandArgs) = parser.parse_args()
855 commandOptions = DummyOptions()
858 if commandOptions.test: