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
163 # 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 # Setup maemo specifics
188 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
189 device = osso.DeviceState(self._osso)
190 device.set_device_state_callback(self._on_device_state_change, 0)
192 logging.warning("No device state support")
196 self._alarmHandler = alarm_handler.AlarmHandler()
200 with gtk_toolbox.gtk_lock():
201 self._errorDisplay.push_exception()
203 logging.warning("No notification support")
204 if hildonize.IS_HILDON:
206 self._ledHandler = led_handler.LedHandler()
208 # Setup maemo specifics
213 self._connection = None
214 if conic is not None:
215 self._connection = conic.Connection()
216 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
217 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
219 logging.warning("No connection support")
221 # Setup costly backends
227 os.makedirs(constants._data_path_)
231 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
233 self._phoneBackends.update({
234 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
236 with gtk_toolbox.gtk_lock():
237 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
238 self._dialpads.update({
239 self.GV_BACKEND: unifiedDialpad,
241 self._accountViews.update({
242 self.GV_BACKEND: gv_views.AccountInfo(
243 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
246 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
247 self._recentViews.update({
248 self.GV_BACKEND: gv_views.RecentCallsView(
249 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
252 self._messagesViews.update({
253 self.GV_BACKEND: gv_views.MessagesView(
254 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
257 self._contactsViews.update({
258 self.GV_BACKEND: gv_views.ContactsView(
259 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
263 fsContactsPath = os.path.join(constants._data_path_, "contacts")
264 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
266 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
267 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
268 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
269 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
272 self._phoneBackends[self.GV_BACKEND],
275 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
276 self._contactsViews[self.GV_BACKEND].append(mergedBook)
277 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
278 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
281 "on_paste": self._on_paste,
282 "on_refresh": self._on_menu_refresh,
283 "on_clearcookies_clicked": self._on_clearcookies_clicked,
284 "on_notebook_switch_page": self._on_notebook_switch_page,
285 "on_about_activate": self._on_about_activate,
287 self._widgetTree.signal_autoconnect(callbackMapping)
289 with gtk_toolbox.gtk_lock():
290 self._originalCurrentLabels = [
291 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
292 for pageIndex in xrange(self._notebook.get_n_pages())
294 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
295 self._notebookTapHandler.enable()
296 self._notebookTapHandler.on_tap = self._reset_tab_refresh
297 self._notebookTapHandler.on_hold = self._on_tab_refresh
298 self._notebookTapHandler.on_holding = self._set_tab_refresh
299 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
301 self._initDone = True
303 config = ConfigParser.SafeConfigParser()
304 config.read(constants._user_settings_)
305 with gtk_toolbox.gtk_lock():
306 self.load_settings(config)
308 self._spawn_attempt_login(2)
310 with gtk_toolbox.gtk_lock():
311 self._errorDisplay.push_exception()
313 def attempt_login(self, numOfAttempts = 10, force = False):
315 @todo Handle user notification better like attempting to login and failed login
317 @note This must be run outside of the UI lock
320 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
321 assert self._initDone, "Attempting login before app is fully loaded"
323 serviceId = self.NULL_BACKEND
327 self.refresh_session()
328 serviceId = self._defaultBackendId
330 except StandardError, e:
331 logging.exception('Session refresh failed with the following message "%s"' % e.message)
334 loggedIn, serviceId = self._login_by_user(numOfAttempts)
336 with gtk_toolbox.gtk_lock():
337 self._change_loggedin_status(serviceId)
338 except StandardError, e:
339 with gtk_toolbox.gtk_lock():
340 self._errorDisplay.push_exception()
342 def _spawn_attempt_login(self, *args):
343 self._loginSink.send(args)
345 def refresh_session(self):
347 @note Thread agnostic
349 assert self._initDone, "Attempting login before app is fully loaded"
353 loggedIn = self._login_by_cookie()
355 loggedIn = self._login_by_settings()
358 raise RuntimeError("Login Failed")
360 def _login_by_cookie(self):
362 @note Thread agnostic
364 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
366 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
369 def _login_by_settings(self):
371 @note Thread agnostic
373 username, password = self._credentials
374 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
376 self._credentials = username, password
377 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
380 def _login_by_user(self, numOfAttempts):
382 @note This must be run outside of the UI lock
384 loggedIn, (username, password) = False, self._credentials
385 tmpServiceId = self.GV_BACKEND
386 for attemptCount in xrange(numOfAttempts):
389 with gtk_toolbox.gtk_lock():
390 credentials = self._credentialsDialog.request_credentials(
391 defaultCredentials = self._credentials
393 username, password = credentials
394 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
397 serviceId = tmpServiceId
398 self._credentials = username, password
399 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
401 serviceId = self.NULL_BACKEND
403 return loggedIn, serviceId
405 def _select_action(self, action, number, message):
406 self.refresh_session()
407 if action == "select":
408 self._dialpads[self._selectedBackendId].set_number(number)
409 self._notebook.set_current_page(self.KEYPAD_TAB)
410 elif action == "dial":
411 self._on_dial_clicked(number)
412 elif action == "sms":
413 self._on_sms_clicked(number, message)
415 assert False, "Unknown action: %s" % action
417 def _change_loggedin_status(self, newStatus):
418 oldStatus = self._selectedBackendId
419 if oldStatus == newStatus:
422 self._dialpads[oldStatus].disable()
423 self._accountViews[oldStatus].disable()
424 self._recentViews[oldStatus].disable()
425 self._messagesViews[oldStatus].disable()
426 self._contactsViews[oldStatus].disable()
428 self._dialpads[newStatus].enable()
429 self._accountViews[newStatus].enable()
430 self._recentViews[newStatus].enable()
431 self._messagesViews[newStatus].enable()
432 self._contactsViews[newStatus].enable()
434 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
435 self._phoneBackends[self._selectedBackendId].set_sane_callback()
436 self._accountViews[self._selectedBackendId].update()
438 self._selectedBackendId = newStatus
440 def load_settings(self, config):
445 self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
447 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
448 for i in xrange(len(self._credentials))
451 base64.b64decode(blob)
454 self._credentials = tuple(creds)
456 if self._alarmHandler is not None:
457 self._alarmHandler.load_settings(config, "alarm")
458 except ConfigParser.NoOptionError, e:
460 "Settings file %s is missing section %s" % (
461 constants._user_settings_,
465 except ConfigParser.NoSectionError, e:
467 "Settings file %s is missing section %s" % (
468 constants._user_settings_,
473 for backendId, view in itertools.chain(
474 self._dialpads.iteritems(),
475 self._accountViews.iteritems(),
476 self._messagesViews.iteritems(),
477 self._recentViews.iteritems(),
478 self._contactsViews.iteritems(),
480 sectionName = "%s - %s" % (backendId, view.name())
482 view.load_settings(config, sectionName)
483 except ConfigParser.NoOptionError, e:
485 "Settings file %s is missing section %s" % (
486 constants._user_settings_,
490 except ConfigParser.NoSectionError, e:
492 "Settings file %s is missing section %s" % (
493 constants._user_settings_,
498 def save_settings(self, config):
500 @note Thread Agnostic
502 config.add_section(constants.__pretty_app_name__)
503 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
504 for i, value in enumerate(self._credentials):
505 blob = base64.b64encode(value)
506 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
507 config.add_section("alarm")
508 if self._alarmHandler is not None:
509 self._alarmHandler.save_settings(config, "alarm")
511 for backendId, view in itertools.chain(
512 self._dialpads.iteritems(),
513 self._accountViews.iteritems(),
514 self._messagesViews.iteritems(),
515 self._recentViews.iteritems(),
516 self._contactsViews.iteritems(),
518 sectionName = "%s - %s" % (backendId, view.name())
519 config.add_section(sectionName)
520 view.save_settings(config, sectionName)
522 def _save_settings(self):
524 @note Thread Agnostic
526 config = ConfigParser.SafeConfigParser()
527 self.save_settings(config)
528 with open(constants._user_settings_, "wb") as configFile:
529 config.write(configFile)
531 def _refresh_active_tab(self):
532 pageIndex = self._notebook.get_current_page()
533 if pageIndex == self.CONTACTS_TAB:
534 self._contactsViews[self._selectedBackendId].update(force=True)
535 elif pageIndex == self.RECENT_TAB:
536 self._recentViews[self._selectedBackendId].update(force=True)
537 elif pageIndex == self.MESSAGES_TAB:
538 self._messagesViews[self._selectedBackendId].update(force=True)
540 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
541 if self._ledHandler is not None:
542 self._ledHandler.off()
544 def _on_close(self, *args, **kwds):
546 if self._osso is not None:
550 self._save_settings()
554 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
556 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
557 For system_inactivity, we have no background tasks to pause
559 @note Hildon specific
562 for backendId in self.BACKENDS:
563 self._phoneBackends[backendId].clear_caches()
564 self._contactsViews[self._selectedBackendId].clear_caches()
567 if save_unsaved_data or shutdown:
568 self._save_settings()
570 def _on_connection_change(self, connection, event, magicIdentifier):
572 @note Hildon specific
576 status = event.get_status()
577 error = event.get_error()
578 iap_id = event.get_iap_id()
579 bearer = event.get_bearer_type()
581 if status == conic.STATUS_CONNECTED:
583 self._spawn_attempt_login(2)
584 elif status == conic.STATUS_DISCONNECTED:
586 self._defaultBackendId = self._selectedBackendId
587 self._change_loggedin_status(self.NULL_BACKEND)
589 def _on_window_state_change(self, widget, event, *args):
591 @note Hildon specific
593 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
594 self._isFullScreen = True
596 self._isFullScreen = False
598 def _on_key_press(self, widget, event, *args):
600 @note Hildon specific
602 if event.keyval == gtk.keysyms.F6:
603 if self._isFullScreen:
604 self._window.unfullscreen()
606 self._window.fullscreen()
608 def _on_clearcookies_clicked(self, *args):
609 self._phoneBackends[self._selectedBackendId].logout()
610 self._accountViews[self._selectedBackendId].clear()
611 self._recentViews[self._selectedBackendId].clear()
612 self._messagesViews[self._selectedBackendId].clear()
613 self._contactsViews[self._selectedBackendId].clear()
614 self._change_loggedin_status(self.NULL_BACKEND)
616 self._spawn_attempt_login(2, True)
618 def _on_notebook_switch_page(self, notebook, page, pageIndex):
619 self._reset_tab_refresh()
621 didRecentUpdate = False
622 didMessagesUpdate = False
624 if pageIndex == self.RECENT_TAB:
625 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
626 elif pageIndex == self.MESSAGES_TAB:
627 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
628 elif pageIndex == self.CONTACTS_TAB:
629 self._contactsViews[self._selectedBackendId].update()
630 elif pageIndex == self.ACCOUNT_TAB:
631 self._accountViews[self._selectedBackendId].update()
633 if didRecentUpdate or didMessagesUpdate:
634 if self._ledHandler is not None:
635 self._ledHandler.off()
637 def _set_tab_refresh(self, *args):
638 pageIndex = self._notebook.get_current_page()
639 child = self._notebook.get_nth_page(pageIndex)
640 self._notebook.get_tab_label(child).set_text("Refresh?")
643 def _reset_tab_refresh(self, *args):
644 pageIndex = self._notebook.get_current_page()
645 child = self._notebook.get_nth_page(pageIndex)
646 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
649 def _on_tab_refresh(self, *args):
650 self._refresh_active_tab()
651 self._reset_tab_refresh()
654 def _on_sms_clicked(self, number, message):
655 assert number, "No number specified"
656 assert message, "Empty message"
658 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
659 except StandardError, e:
661 self._errorDisplay.push_exception()
665 self._errorDisplay.push_message(
666 "Backend link with grandcentral is not working, please try again"
672 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
674 except StandardError, e:
675 self._errorDisplay.push_exception()
676 except ValueError, e:
677 self._errorDisplay.push_exception()
680 self._dialpads[self._selectedBackendId].clear()
682 def _on_dial_clicked(self, number):
683 assert number, "No number to call"
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 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
700 self._phoneBackends[self._selectedBackendId].dial(number)
702 except StandardError, e:
703 self._errorDisplay.push_exception()
704 except ValueError, e:
705 self._errorDisplay.push_exception()
708 self._dialpads[self._selectedBackendId].clear()
710 def _on_menu_refresh(self, *args):
711 self._refresh_active_tab()
713 def _on_paste(self, *args):
714 contents = self._clipboard.wait_for_text()
715 self._dialpads[self._selectedBackendId].set_number(contents)
717 def _on_about_activate(self, *args):
718 dlg = gtk.AboutDialog()
719 dlg.set_name(constants.__pretty_app_name__)
720 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
721 dlg.set_copyright("Copyright 2008 - LGPL")
722 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")
723 dlg.set_website("http://gc-dialer.garage.maemo.org/")
724 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
732 failureCount, testCount = doctest.testmod()
734 print "Tests Successful"
741 _lock_file = os.path.join(constants._data_path_, ".lock")
742 #with gtk_toolbox.flock(_lock_file, 0):
743 gtk.gdk.threads_init()
745 if hildonize.IS_HILDON:
746 gtk.set_application_name(constants.__pretty_app_name__)
747 handle = Dialcentral()
751 class DummyOptions(object):
757 if __name__ == "__main__":
758 if hildonize.IS_HILDON:
759 userLogPath = "%s/dialcentral.log" % constants._data_path_
760 logging.basicConfig(level=logging.DEBUG, filename=userLogPath)
762 logging.basicConfig(level=logging.DEBUG)
764 if len(sys.argv) > 1:
770 if optparse is not None:
771 parser = optparse.OptionParser()
772 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
773 (commandOptions, commandArgs) = parser.parse_args()
775 commandOptions = DummyOptions()
778 if commandOptions.test: