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
23 from __future__ import with_statement
42 def getmtime_nothrow(path):
44 return os.path.getmtime(path)
49 def display_error_message(msg):
50 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
52 def close(dialog, response):
54 error_dialog.connect("response", close)
58 class Dialcentral(object):
61 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
62 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
63 '/usr/lib/dialcentral/dialcentral.glade',
74 BACKENDS = (NULL_BACKEND, GV_BACKEND)
77 self._initDone = False
78 self._connection = None
80 self._clipboard = gtk.clipboard_get()
82 self._credentials = ("", "")
83 self._selectedBackendId = self.NULL_BACKEND
84 self._defaultBackendId = self.GV_BACKEND
85 self._phoneBackends = None
87 self._accountViews = None
88 self._messagesViews = None
89 self._recentViews = None
90 self._contactsViews = None
91 self._alarmHandler = None
92 self._ledHandler = None
93 self._originalCurrentLabels = []
95 for path in self._glade_files:
96 if os.path.isfile(path):
97 self._widgetTree = gtk.glade.XML(path)
100 display_error_message("Cannot find dialcentral.glade")
104 self._window = self._widgetTree.get_widget("mainWindow")
105 self._notebook = self._widgetTree.get_widget("notebook")
106 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
107 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
109 self._isFullScreen = False
110 self._app = hildonize.get_app_class()()
111 self._window = hildonize.hildonize_window(self._app, self._window)
112 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
113 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
114 hildonize.hildonize_combo_entry(self._widgetTree.get_widget("callbackcombo").get_child())
116 for scrollingWidget in (
117 'recent_scrolledwindow',
118 'message_scrolledwindow',
119 'contacts_scrolledwindow',
120 "phoneSelectionMessage_scrolledwindow",
121 "phonetypes_scrolledwindow",
122 "smsMessage_scrolledwindow",
123 "smsMessage_scrolledEntry",
125 hildonize.set_thumb_scrollbar(self._widgetTree.get_widget(scrollingWidget))
127 hildonize.hildonize_menu(self._window, self._widgetTree.get_widget("dialpad_menubar"))
129 if hildonize.IS_HILDON:
130 self._window.connect("key-press-event", self._on_key_press)
131 self._window.connect("window-state-event", self._on_window_state_change)
133 pass # warnings.warn("No Hildon", UserWarning, 2)
135 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
138 "on_dialpad_quit": self._on_close,
140 self._widgetTree.signal_autoconnect(callbackMapping)
142 self._window.connect("destroy", self._on_close)
143 self._window.set_default_size(800, 300)
144 self._window.show_all()
146 self._loginSink = gtk_toolbox.threaded_stage(
149 gtk_toolbox.null_sink(),
153 backgroundSetup = threading.Thread(target=self._idle_setup)
154 backgroundSetup.setDaemon(True)
155 backgroundSetup.start()
157 def _idle_setup(self):
159 If something can be done after the UI loads, push it here so it's not blocking the UI
162 # Barebones UI handlers
166 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
167 with gtk_toolbox.gtk_lock():
168 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
169 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
170 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
171 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
172 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
174 self._dialpads[self._selectedBackendId].enable()
175 self._accountViews[self._selectedBackendId].enable()
176 self._recentViews[self._selectedBackendId].enable()
177 self._messagesViews[self._selectedBackendId].enable()
178 self._contactsViews[self._selectedBackendId].enable()
180 # Setup maemo specifics
187 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
188 device = osso.DeviceState(self._osso)
189 device.set_device_state_callback(self._on_device_state_change, 0)
191 pass # warnings.warn("No OSSO", UserWarning, 2)
195 self._alarmHandler = alarm_handler.AlarmHandler()
199 with gtk_toolbox.gtk_lock():
200 self._errorDisplay.push_exception()
202 if hildonize.IS_HILDON:
204 self._ledHandler = led_handler.LedHandler()
206 # Setup maemo specifics
211 self._connection = None
212 if conic is not None:
213 self._connection = conic.Connection()
214 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
215 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
217 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
219 # Setup costly backends
225 os.makedirs(constants._data_path_)
229 gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
230 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
231 self._defaultBackendId = self._guess_preferred_backend((
232 (self.GV_BACKEND, gvCookiePath),
235 self._phoneBackends.update({
236 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
238 with gtk_toolbox.gtk_lock():
239 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
240 unifiedDialpad.set_number("")
241 self._dialpads.update({
242 self.GV_BACKEND: unifiedDialpad,
244 self._accountViews.update({
245 self.GV_BACKEND: gv_views.AccountInfo(
246 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
249 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
250 self._recentViews.update({
251 self.GV_BACKEND: gv_views.RecentCallsView(
252 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
255 self._messagesViews.update({
256 self.GV_BACKEND: gv_views.MessagesView(
257 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
260 self._contactsViews.update({
261 self.GV_BACKEND: gv_views.ContactsView(
262 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
266 fsContactsPath = os.path.join(constants._data_path_, "contacts")
267 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
268 for backendId in (self.GV_BACKEND, ):
269 self._dialpads[backendId].number_selected = self._select_action
270 self._recentViews[backendId].number_selected = self._select_action
271 self._messagesViews[backendId].number_selected = self._select_action
272 self._contactsViews[backendId].number_selected = self._select_action
275 self._phoneBackends[backendId],
278 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
279 self._contactsViews[backendId].append(mergedBook)
280 self._contactsViews[backendId].extend(addressBooks)
281 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
284 "on_paste": self._on_paste,
285 "on_refresh": self._on_menu_refresh,
286 "on_clearcookies_clicked": self._on_clearcookies_clicked,
287 "on_notebook_switch_page": self._on_notebook_switch_page,
288 "on_about_activate": self._on_about_activate,
290 self._widgetTree.signal_autoconnect(callbackMapping)
292 with gtk_toolbox.gtk_lock():
293 self._originalCurrentLabels = [
294 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
295 for pageIndex in xrange(self._notebook.get_n_pages())
297 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
298 self._notebookTapHandler.enable()
299 self._notebookTapHandler.on_tap = self._reset_tab_refresh
300 self._notebookTapHandler.on_hold = self._on_tab_refresh
301 self._notebookTapHandler.on_holding = self._set_tab_refresh
302 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
304 self._initDone = True
306 config = ConfigParser.SafeConfigParser()
307 config.read(constants._user_settings_)
308 with gtk_toolbox.gtk_lock():
309 self.load_settings(config)
311 self._spawn_attempt_login(2)
313 with gtk_toolbox.gtk_lock():
314 self._errorDisplay.push_exception()
316 def attempt_login(self, numOfAttempts = 10, force = False):
318 @todo Handle user notification better like attempting to login and failed login
320 @note This must be run outside of the UI lock
323 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
324 assert self._initDone, "Attempting login before app is fully loaded"
326 serviceId = self.NULL_BACKEND
330 self.refresh_session()
331 serviceId = self._defaultBackendId
333 except StandardError, e:
334 warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
337 loggedIn, serviceId = self._login_by_user(numOfAttempts)
339 with gtk_toolbox.gtk_lock():
340 self._change_loggedin_status(serviceId)
341 except StandardError, e:
342 with gtk_toolbox.gtk_lock():
343 self._errorDisplay.push_exception()
345 def _spawn_attempt_login(self, *args):
346 self._loginSink.send(args)
348 def refresh_session(self):
350 @note Thread agnostic
352 assert self._initDone, "Attempting login before app is fully loaded"
356 loggedIn = self._login_by_cookie()
358 loggedIn = self._login_by_settings()
361 raise RuntimeError("Login Failed")
363 def _login_by_cookie(self):
365 @note Thread agnostic
367 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
370 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
375 def _login_by_settings(self):
377 @note Thread agnostic
379 username, password = self._credentials
380 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
382 self._credentials = username, password
384 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
389 def _login_by_user(self, numOfAttempts):
391 @note This must be run outside of the UI lock
393 loggedIn, (username, password) = False, self._credentials
394 tmpServiceId = self.NULL_BACKEND
395 for attemptCount in xrange(numOfAttempts):
398 availableServices = (
399 (self.GV_BACKEND, "Google Voice"),
401 with gtk_toolbox.gtk_lock():
402 credentials = self._credentialsDialog.request_credentials_from(
403 availableServices, defaultCredentials = self._credentials
405 tmpServiceId, username, password = credentials
406 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
409 serviceId = tmpServiceId
410 self._credentials = username, password
412 "Logged into %r through user request" % self._phoneBackends[serviceId],
416 serviceId = self.NULL_BACKEND
418 return loggedIn, serviceId
420 def _select_action(self, action, number, message):
421 self.refresh_session()
422 if action == "select":
423 self._dialpads[self._selectedBackendId].set_number(number)
424 self._notebook.set_current_page(self.KEYPAD_TAB)
425 elif action == "dial":
426 self._on_dial_clicked(number)
427 elif action == "sms":
428 self._on_sms_clicked(number, message)
430 assert False, "Unknown action: %s" % action
432 def _change_loggedin_status(self, newStatus):
433 oldStatus = self._selectedBackendId
434 if oldStatus == newStatus:
437 self._dialpads[oldStatus].disable()
438 self._accountViews[oldStatus].disable()
439 self._recentViews[oldStatus].disable()
440 self._messagesViews[oldStatus].disable()
441 self._contactsViews[oldStatus].disable()
443 self._dialpads[newStatus].enable()
444 self._accountViews[newStatus].enable()
445 self._recentViews[newStatus].enable()
446 self._messagesViews[newStatus].enable()
447 self._contactsViews[newStatus].enable()
449 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
450 self._phoneBackends[self._selectedBackendId].set_sane_callback()
451 self._accountViews[self._selectedBackendId].update()
453 self._selectedBackendId = newStatus
455 def load_settings(self, config):
460 self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
462 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
463 for i in xrange(len(self._credentials))
466 base64.b64decode(blob)
469 self._credentials = tuple(creds)
471 if self._alarmHandler is not None:
472 self._alarmHandler.load_settings(config, "alarm")
473 except ConfigParser.NoOptionError, e:
475 "Settings file %s is missing section %s" % (
476 constants._user_settings_,
481 except ConfigParser.NoSectionError, e:
483 "Settings file %s is missing section %s" % (
484 constants._user_settings_,
490 for backendId, view in itertools.chain(
491 self._dialpads.iteritems(),
492 self._accountViews.iteritems(),
493 self._messagesViews.iteritems(),
494 self._recentViews.iteritems(),
495 self._contactsViews.iteritems(),
497 sectionName = "%s - %s" % (backendId, view.name())
499 view.load_settings(config, sectionName)
500 except ConfigParser.NoOptionError, e:
502 "Settings file %s is missing section %s" % (
503 constants._user_settings_,
508 except ConfigParser.NoSectionError, e:
510 "Settings file %s is missing section %s" % (
511 constants._user_settings_,
517 def save_settings(self, config):
519 @note Thread Agnostic
521 config.add_section(constants.__pretty_app_name__)
522 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
523 for i, value in enumerate(self._credentials):
524 blob = base64.b64encode(value)
525 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
526 config.add_section("alarm")
527 if self._alarmHandler is not None:
528 self._alarmHandler.save_settings(config, "alarm")
530 for backendId, view in itertools.chain(
531 self._dialpads.iteritems(),
532 self._accountViews.iteritems(),
533 self._messagesViews.iteritems(),
534 self._recentViews.iteritems(),
535 self._contactsViews.iteritems(),
537 sectionName = "%s - %s" % (backendId, view.name())
538 config.add_section(sectionName)
539 view.save_settings(config, sectionName)
541 def _guess_preferred_backend(self, backendAndCookiePaths):
543 (getmtime_nothrow(path), backendId, path)
544 for backendId, path in backendAndCookiePaths
546 modTimeAndPath.sort()
547 return modTimeAndPath[-1][1]
549 def _save_settings(self):
551 @note Thread Agnostic
553 config = ConfigParser.SafeConfigParser()
554 self.save_settings(config)
555 with open(constants._user_settings_, "wb") as configFile:
556 config.write(configFile)
558 def _refresh_active_tab(self):
559 pageIndex = self._notebook.get_current_page()
560 if pageIndex == self.CONTACTS_TAB:
561 self._contactsViews[self._selectedBackendId].update(force=True)
562 elif pageIndex == self.RECENT_TAB:
563 self._recentViews[self._selectedBackendId].update(force=True)
564 elif pageIndex == self.MESSAGES_TAB:
565 self._messagesViews[self._selectedBackendId].update(force=True)
567 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
568 if self._ledHandler is not None:
569 self._ledHandler.off()
571 def _on_close(self, *args, **kwds):
573 if self._osso is not None:
577 self._save_settings()
581 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
583 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
584 For system_inactivity, we have no background tasks to pause
586 @note Hildon specific
589 for backendId in self.BACKENDS:
590 self._phoneBackends[backendId].clear_caches()
591 self._contactsViews[self._selectedBackendId].clear_caches()
594 if save_unsaved_data or shutdown:
595 self._save_settings()
597 def _on_connection_change(self, connection, event, magicIdentifier):
599 @note Hildon specific
603 status = event.get_status()
604 error = event.get_error()
605 iap_id = event.get_iap_id()
606 bearer = event.get_bearer_type()
608 if status == conic.STATUS_CONNECTED:
610 self._spawn_attempt_login(2)
611 elif status == conic.STATUS_DISCONNECTED:
613 self._defaultBackendId = self._selectedBackendId
614 self._change_loggedin_status(self.NULL_BACKEND)
616 def _on_window_state_change(self, widget, event, *args):
618 @note Hildon specific
620 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
621 self._isFullScreen = True
623 self._isFullScreen = False
625 def _on_key_press(self, widget, event, *args):
627 @note Hildon specific
629 if event.keyval == gtk.keysyms.F6:
630 if self._isFullScreen:
631 self._window.unfullscreen()
633 self._window.fullscreen()
635 def _on_clearcookies_clicked(self, *args):
636 self._phoneBackends[self._selectedBackendId].logout()
637 self._accountViews[self._selectedBackendId].clear()
638 self._recentViews[self._selectedBackendId].clear()
639 self._messagesViews[self._selectedBackendId].clear()
640 self._contactsViews[self._selectedBackendId].clear()
641 self._change_loggedin_status(self.NULL_BACKEND)
643 self._spawn_attempt_login(2, True)
645 def _on_notebook_switch_page(self, notebook, page, pageIndex):
646 self._reset_tab_refresh()
648 didRecentUpdate = False
649 didMessagesUpdate = False
651 if pageIndex == self.RECENT_TAB:
652 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
653 elif pageIndex == self.MESSAGES_TAB:
654 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
655 elif pageIndex == self.CONTACTS_TAB:
656 self._contactsViews[self._selectedBackendId].update()
657 elif pageIndex == self.ACCOUNT_TAB:
658 self._accountViews[self._selectedBackendId].update()
660 if didRecentUpdate or didMessagesUpdate:
661 if self._ledHandler is not None:
662 self._ledHandler.off()
664 def _set_tab_refresh(self, *args):
665 pageIndex = self._notebook.get_current_page()
666 child = self._notebook.get_nth_page(pageIndex)
667 self._notebook.get_tab_label(child).set_text("Refresh?")
670 def _reset_tab_refresh(self, *args):
671 pageIndex = self._notebook.get_current_page()
672 child = self._notebook.get_nth_page(pageIndex)
673 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
676 def _on_tab_refresh(self, *args):
677 self._refresh_active_tab()
678 self._reset_tab_refresh()
681 def _on_sms_clicked(self, number, message):
682 assert number, "No number specified"
683 assert message, "Empty message"
685 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
686 except StandardError, e:
688 self._errorDisplay.push_exception()
692 self._errorDisplay.push_message(
693 "Backend link with grandcentral is not working, please try again"
699 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
701 except StandardError, e:
702 self._errorDisplay.push_exception()
703 except ValueError, e:
704 self._errorDisplay.push_exception()
707 self._dialpads[self._selectedBackendId].clear()
709 def _on_dial_clicked(self, number):
710 assert number, "No number to call"
712 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
713 except StandardError, e:
715 self._errorDisplay.push_exception()
719 self._errorDisplay.push_message(
720 "Backend link with grandcentral is not working, please try again"
726 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
727 self._phoneBackends[self._selectedBackendId].dial(number)
729 except StandardError, e:
730 self._errorDisplay.push_exception()
731 except ValueError, e:
732 self._errorDisplay.push_exception()
735 self._dialpads[self._selectedBackendId].clear()
737 def _on_menu_refresh(self, *args):
738 self._refresh_active_tab()
740 def _on_paste(self, *args):
741 contents = self._clipboard.wait_for_text()
742 self._dialpads[self._selectedBackendId].set_number(contents)
744 def _on_about_activate(self, *args):
745 dlg = gtk.AboutDialog()
746 dlg.set_name(constants.__pretty_app_name__)
747 dlg.set_version(constants.__version__)
748 dlg.set_copyright("Copyright 2008 - LGPL")
749 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")
750 dlg.set_website("http://gc-dialer.garage.maemo.org/")
751 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
759 failureCount, testCount = doctest.testmod()
761 print "Tests Successful"
768 _lock_file = os.path.join(constants._data_path_, ".lock")
769 #with gtk_toolbox.flock(_lock_file, 0):
770 gtk.gdk.threads_init()
772 if hildonize.IS_HILDON:
773 gtk.set_application_name(constants.__pretty_app_name__)
774 handle = Dialcentral()
778 class DummyOptions(object):
784 if __name__ == "__main__":
785 if len(sys.argv) > 1:
791 if optparse is not None:
792 parser = optparse.OptionParser()
793 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
794 (commandOptions, commandArgs) = parser.parse_args()
796 commandOptions = DummyOptions()
799 if commandOptions.test: