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 logging.warning("No hildonization support")
136 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
139 "on_dialpad_quit": self._on_close,
141 self._widgetTree.signal_autoconnect(callbackMapping)
143 self._window.connect("destroy", self._on_close)
144 self._window.set_default_size(800, 300)
145 self._window.show_all()
147 self._loginSink = gtk_toolbox.threaded_stage(
150 gtk_toolbox.null_sink(),
154 backgroundSetup = threading.Thread(target=self._idle_setup)
155 backgroundSetup.setDaemon(True)
156 backgroundSetup.start()
158 def _idle_setup(self):
160 If something can be done after the UI loads, push it here so it's not blocking the UI
162 # Barebones UI handlers
167 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
168 with gtk_toolbox.gtk_lock():
169 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
170 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
171 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
172 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
173 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
175 self._dialpads[self._selectedBackendId].enable()
176 self._accountViews[self._selectedBackendId].enable()
177 self._recentViews[self._selectedBackendId].enable()
178 self._messagesViews[self._selectedBackendId].enable()
179 self._contactsViews[self._selectedBackendId].enable()
181 with gtk_toolbox.gtk_lock():
182 self._errorDisplay.push_exception()
184 # Setup maemo specifics
188 except (ImportError, OSError):
192 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
193 device = osso.DeviceState(self._osso)
194 device.set_device_state_callback(self._on_device_state_change, 0)
196 logging.warning("No device state support")
200 self._alarmHandler = alarm_handler.AlarmHandler()
201 except (ImportError, OSError):
204 with gtk_toolbox.gtk_lock():
205 self._errorDisplay.push_exception()
207 logging.warning("No notification support")
208 if hildonize.IS_HILDON:
210 self._ledHandler = led_handler.LedHandler()
214 except (ImportError, OSError):
216 self._connection = None
217 if conic is not None:
218 self._connection = conic.Connection()
219 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
220 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
222 logging.warning("No connection support")
224 with gtk_toolbox.gtk_lock():
225 self._errorDisplay.push_exception()
227 # Setup costly backends
234 os.makedirs(constants._data_path_)
238 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
240 self._phoneBackends.update({
241 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
243 with gtk_toolbox.gtk_lock():
244 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
245 self._dialpads.update({
246 self.GV_BACKEND: unifiedDialpad,
248 self._accountViews.update({
249 self.GV_BACKEND: gv_views.AccountInfo(
250 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
253 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
254 self._recentViews.update({
255 self.GV_BACKEND: gv_views.RecentCallsView(
256 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
259 self._messagesViews.update({
260 self.GV_BACKEND: gv_views.MessagesView(
261 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
264 self._contactsViews.update({
265 self.GV_BACKEND: gv_views.ContactsView(
266 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
270 fsContactsPath = os.path.join(constants._data_path_, "contacts")
271 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
273 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
274 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
275 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
276 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
279 self._phoneBackends[self.GV_BACKEND],
282 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
283 self._contactsViews[self.GV_BACKEND].append(mergedBook)
284 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
285 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
288 "on_paste": self._on_paste,
289 "on_refresh": self._on_menu_refresh,
290 "on_rotate": self._on_menu_rotate,
291 "on_clearcookies_clicked": self._on_clearcookies_clicked,
292 "on_notebook_switch_page": self._on_notebook_switch_page,
293 "on_about_activate": self._on_about_activate,
295 self._widgetTree.signal_autoconnect(callbackMapping)
297 with gtk_toolbox.gtk_lock():
298 self._originalCurrentLabels = [
299 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
300 for pageIndex in xrange(self._notebook.get_n_pages())
302 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
303 self._notebookTapHandler.enable()
304 self._notebookTapHandler.on_tap = self._reset_tab_refresh
305 self._notebookTapHandler.on_hold = self._on_tab_refresh
306 self._notebookTapHandler.on_holding = self._set_tab_refresh
307 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
309 self._initDone = True
311 config = ConfigParser.SafeConfigParser()
312 config.read(constants._user_settings_)
313 with gtk_toolbox.gtk_lock():
314 self.load_settings(config)
316 with gtk_toolbox.gtk_lock():
317 self._errorDisplay.push_exception()
319 self._spawn_attempt_login(2)
321 def attempt_login(self, numOfAttempts = 10, force = False):
323 @todo Handle user notification better like attempting to login and failed login
325 @note This must be run outside of the UI lock
328 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
329 assert self._initDone, "Attempting login before app is fully loaded"
331 serviceId = self.NULL_BACKEND
335 self.refresh_session()
336 serviceId = self._defaultBackendId
339 logging.exception('Session refresh failed with the following message "%s"' % e.message)
342 loggedIn, serviceId = self._login_by_user(numOfAttempts)
344 with gtk_toolbox.gtk_lock():
345 self._change_loggedin_status(serviceId)
347 with gtk_toolbox.gtk_lock():
348 self._errorDisplay.push_exception()
350 def _spawn_attempt_login(self, *args):
351 self._loginSink.send(args)
353 def refresh_session(self):
355 @note Thread agnostic
357 assert self._initDone, "Attempting login before app is fully loaded"
361 loggedIn = self._login_by_cookie()
363 loggedIn = self._login_by_settings()
366 raise RuntimeError("Login Failed")
368 def _login_by_cookie(self):
370 @note Thread agnostic
372 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
374 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
377 def _login_by_settings(self):
379 @note Thread agnostic
381 username, password = self._credentials
382 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
384 self._credentials = username, password
385 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
388 def _login_by_user(self, numOfAttempts):
390 @note This must be run outside of the UI lock
392 loggedIn, (username, password) = False, self._credentials
393 tmpServiceId = self.GV_BACKEND
394 for attemptCount in xrange(numOfAttempts):
397 with gtk_toolbox.gtk_lock():
398 credentials = self._credentialsDialog.request_credentials(
399 defaultCredentials = self._credentials
401 username, password = credentials
402 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
405 serviceId = tmpServiceId
406 self._credentials = username, password
407 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
409 serviceId = self.NULL_BACKEND
411 return loggedIn, serviceId
413 def _select_action(self, action, number, message):
414 self.refresh_session()
415 if action == "select":
416 self._dialpads[self._selectedBackendId].set_number(number)
417 self._notebook.set_current_page(self.KEYPAD_TAB)
418 elif action == "dial":
419 self._on_dial_clicked(number)
420 elif action == "sms":
421 self._on_sms_clicked(number, message)
423 assert False, "Unknown action: %s" % action
425 def _change_loggedin_status(self, newStatus):
426 oldStatus = self._selectedBackendId
427 if oldStatus == newStatus:
430 self._dialpads[oldStatus].disable()
431 self._accountViews[oldStatus].disable()
432 self._recentViews[oldStatus].disable()
433 self._messagesViews[oldStatus].disable()
434 self._contactsViews[oldStatus].disable()
436 self._dialpads[newStatus].enable()
437 self._accountViews[newStatus].enable()
438 self._recentViews[newStatus].enable()
439 self._messagesViews[newStatus].enable()
440 self._contactsViews[newStatus].enable()
442 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
443 self._phoneBackends[self._selectedBackendId].set_sane_callback()
444 self._accountViews[self._selectedBackendId].update()
446 self._selectedBackendId = newStatus
448 def load_settings(self, config):
453 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
455 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
456 for i in xrange(len(self._credentials))
459 base64.b64decode(blob)
462 self._credentials = tuple(creds)
464 if self._alarmHandler is not None:
465 self._alarmHandler.load_settings(config, "alarm")
466 except ConfigParser.NoOptionError, e:
468 "Settings file %s is missing section %s" % (
469 constants._user_settings_,
473 except ConfigParser.NoSectionError, e:
475 "Settings file %s is missing section %s" % (
476 constants._user_settings_,
481 for backendId, view in itertools.chain(
482 self._dialpads.iteritems(),
483 self._accountViews.iteritems(),
484 self._messagesViews.iteritems(),
485 self._recentViews.iteritems(),
486 self._contactsViews.iteritems(),
488 sectionName = "%s - %s" % (backendId, view.name())
490 view.load_settings(config, sectionName)
491 except ConfigParser.NoOptionError, e:
493 "Settings file %s is missing section %s" % (
494 constants._user_settings_,
498 except ConfigParser.NoSectionError, e:
500 "Settings file %s is missing section %s" % (
501 constants._user_settings_,
506 # @todo down here till this issue is fixed
508 previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
509 if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
510 hildonize.window_to_landscape(self._window)
511 elif previousOrientation == gtk.ORIENTATION_VERTICAL:
512 hildonize.window_to_portrait(self._window)
513 except ConfigParser.NoOptionError, e:
515 "Settings file %s is missing section %s" % (
516 constants._user_settings_,
520 except ConfigParser.NoSectionError, e:
522 "Settings file %s is missing section %s" % (
523 constants._user_settings_,
528 def save_settings(self, config):
530 @note Thread Agnostic
532 config.add_section(constants.__pretty_app_name__)
533 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
534 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
535 for i, value in enumerate(self._credentials):
536 blob = base64.b64encode(value)
537 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
538 config.add_section("alarm")
539 if self._alarmHandler is not None:
540 self._alarmHandler.save_settings(config, "alarm")
542 for backendId, view in itertools.chain(
543 self._dialpads.iteritems(),
544 self._accountViews.iteritems(),
545 self._messagesViews.iteritems(),
546 self._recentViews.iteritems(),
547 self._contactsViews.iteritems(),
549 sectionName = "%s - %s" % (backendId, view.name())
550 config.add_section(sectionName)
551 view.save_settings(config, sectionName)
553 def _save_settings(self):
555 @note Thread Agnostic
557 config = ConfigParser.SafeConfigParser()
558 self.save_settings(config)
559 with open(constants._user_settings_, "wb") as configFile:
560 config.write(configFile)
562 def _refresh_active_tab(self):
563 pageIndex = self._notebook.get_current_page()
564 if pageIndex == self.CONTACTS_TAB:
565 self._contactsViews[self._selectedBackendId].update(force=True)
566 elif pageIndex == self.RECENT_TAB:
567 self._recentViews[self._selectedBackendId].update(force=True)
568 elif pageIndex == self.MESSAGES_TAB:
569 self._messagesViews[self._selectedBackendId].update(force=True)
571 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
572 if self._ledHandler is not None:
573 self._ledHandler.off()
575 def _on_close(self, *args, **kwds):
577 if self._osso is not None:
581 self._save_settings()
585 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
587 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
588 For system_inactivity, we have no background tasks to pause
590 @note Hildon specific
594 for backendId in self.BACKENDS:
595 self._phoneBackends[backendId].clear_caches()
596 self._contactsViews[self._selectedBackendId].clear_caches()
599 if save_unsaved_data or shutdown:
600 self._save_settings()
602 self._errorDisplay.push_exception()
604 def _on_connection_change(self, connection, event, magicIdentifier):
606 @note Hildon specific
611 status = event.get_status()
612 error = event.get_error()
613 iap_id = event.get_iap_id()
614 bearer = event.get_bearer_type()
616 if status == conic.STATUS_CONNECTED:
618 self._spawn_attempt_login(2)
619 elif status == conic.STATUS_DISCONNECTED:
621 self._defaultBackendId = self._selectedBackendId
622 self._change_loggedin_status(self.NULL_BACKEND)
624 self._errorDisplay.push_exception()
626 def _on_window_state_change(self, widget, event, *args):
628 @note Hildon specific
631 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
632 self._isFullScreen = True
634 self._isFullScreen = False
636 self._errorDisplay.push_exception()
638 def _on_key_press(self, widget, event, *args):
640 @note Hildon specific
643 if event.keyval == gtk.keysyms.F6:
644 if self._isFullScreen:
645 self._window.unfullscreen()
647 self._window.fullscreen()
649 self._errorDisplay.push_exception()
651 def _on_clearcookies_clicked(self, *args):
653 self._phoneBackends[self._selectedBackendId].logout()
654 self._accountViews[self._selectedBackendId].clear()
655 self._recentViews[self._selectedBackendId].clear()
656 self._messagesViews[self._selectedBackendId].clear()
657 self._contactsViews[self._selectedBackendId].clear()
658 self._change_loggedin_status(self.NULL_BACKEND)
660 self._spawn_attempt_login(2, True)
662 self._errorDisplay.push_exception()
664 def _on_notebook_switch_page(self, notebook, page, pageIndex):
666 self._reset_tab_refresh()
668 didRecentUpdate = False
669 didMessagesUpdate = False
671 if pageIndex == self.RECENT_TAB:
672 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
673 elif pageIndex == self.MESSAGES_TAB:
674 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
675 elif pageIndex == self.CONTACTS_TAB:
676 self._contactsViews[self._selectedBackendId].update()
677 elif pageIndex == self.ACCOUNT_TAB:
678 self._accountViews[self._selectedBackendId].update()
680 if didRecentUpdate or didMessagesUpdate:
681 if self._ledHandler is not None:
682 self._ledHandler.off()
684 self._errorDisplay.push_exception()
686 def _set_tab_refresh(self, *args):
688 pageIndex = self._notebook.get_current_page()
689 child = self._notebook.get_nth_page(pageIndex)
690 self._notebook.get_tab_label(child).set_text("Refresh?")
692 self._errorDisplay.push_exception()
695 def _reset_tab_refresh(self, *args):
697 pageIndex = self._notebook.get_current_page()
698 child = self._notebook.get_nth_page(pageIndex)
699 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
701 self._errorDisplay.push_exception()
704 def _on_tab_refresh(self, *args):
706 self._refresh_active_tab()
707 self._reset_tab_refresh()
709 self._errorDisplay.push_exception()
712 def _on_sms_clicked(self, number, message):
714 assert number, "No number specified"
715 assert message, "Empty message"
717 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
720 self._errorDisplay.push_exception()
724 self._errorDisplay.push_message(
725 "Backend link with grandcentral is not working, please try again"
731 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
734 self._errorDisplay.push_exception()
737 self._dialpads[self._selectedBackendId].clear()
739 self._errorDisplay.push_exception()
741 def _on_dial_clicked(self, number):
743 assert number, "No number to call"
745 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
748 self._errorDisplay.push_exception()
752 self._errorDisplay.push_message(
753 "Backend link with grandcentral is not working, please try again"
759 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
760 self._phoneBackends[self._selectedBackendId].dial(number)
763 self._errorDisplay.push_exception()
766 self._dialpads[self._selectedBackendId].clear()
768 self._errorDisplay.push_exception()
770 def _on_menu_refresh(self, *args):
772 self._refresh_active_tab()
774 self._errorDisplay.push_exception()
776 def _on_menu_rotate(self, *args):
778 orientation = gtk_toolbox.get_screen_orientation()
779 if orientation == gtk.ORIENTATION_HORIZONTAL:
780 hildonize.window_to_portrait(self._window)
781 elif orientation == gtk.ORIENTATION_VERTICAL:
782 hildonize.window_to_landscape(self._window)
784 self._errorDisplay.push_exception()
786 def _on_paste(self, *args):
788 contents = self._clipboard.wait_for_text()
789 self._dialpads[self._selectedBackendId].set_number(contents)
791 self._errorDisplay.push_exception()
793 def _on_about_activate(self, *args):
795 dlg = gtk.AboutDialog()
796 dlg.set_name(constants.__pretty_app_name__)
797 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
798 dlg.set_copyright("Copyright 2008 - LGPL")
799 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")
800 dlg.set_website("http://gc-dialer.garage.maemo.org/")
801 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
805 self._errorDisplay.push_exception()
811 failureCount, testCount = doctest.testmod()
813 print "Tests Successful"
820 _lock_file = os.path.join(constants._data_path_, ".lock")
821 #with gtk_toolbox.flock(_lock_file, 0):
822 gtk.gdk.threads_init()
824 if hildonize.IS_HILDON:
825 gtk.set_application_name(constants.__pretty_app_name__)
826 handle = Dialcentral()
830 class DummyOptions(object):
836 if __name__ == "__main__":
837 logging.basicConfig(level=logging.DEBUG)
839 if len(sys.argv) > 1:
845 if optparse is not None:
846 parser = optparse.OptionParser()
847 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
848 (commandOptions, commandArgs) = parser.parse_args()
850 commandOptions = DummyOptions()
853 if commandOptions.test: