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 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
231 self._phoneBackends.update({
232 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
234 with gtk_toolbox.gtk_lock():
235 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
236 self._dialpads.update({
237 self.GV_BACKEND: unifiedDialpad,
239 self._accountViews.update({
240 self.GV_BACKEND: gv_views.AccountInfo(
241 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
244 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
245 self._recentViews.update({
246 self.GV_BACKEND: gv_views.RecentCallsView(
247 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
250 self._messagesViews.update({
251 self.GV_BACKEND: gv_views.MessagesView(
252 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
255 self._contactsViews.update({
256 self.GV_BACKEND: gv_views.ContactsView(
257 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
261 fsContactsPath = os.path.join(constants._data_path_, "contacts")
262 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
264 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
265 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
266 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
267 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
270 self._phoneBackends[self.GV_BACKEND],
273 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
274 self._contactsViews[self.GV_BACKEND].append(mergedBook)
275 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
276 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
279 "on_paste": self._on_paste,
280 "on_refresh": self._on_menu_refresh,
281 "on_clearcookies_clicked": self._on_clearcookies_clicked,
282 "on_notebook_switch_page": self._on_notebook_switch_page,
283 "on_about_activate": self._on_about_activate,
285 self._widgetTree.signal_autoconnect(callbackMapping)
287 with gtk_toolbox.gtk_lock():
288 self._originalCurrentLabels = [
289 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
290 for pageIndex in xrange(self._notebook.get_n_pages())
292 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
293 self._notebookTapHandler.enable()
294 self._notebookTapHandler.on_tap = self._reset_tab_refresh
295 self._notebookTapHandler.on_hold = self._on_tab_refresh
296 self._notebookTapHandler.on_holding = self._set_tab_refresh
297 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
299 self._initDone = True
301 config = ConfigParser.SafeConfigParser()
302 config.read(constants._user_settings_)
303 with gtk_toolbox.gtk_lock():
304 self.load_settings(config)
306 self._spawn_attempt_login(2)
308 with gtk_toolbox.gtk_lock():
309 self._errorDisplay.push_exception()
311 def attempt_login(self, numOfAttempts = 10, force = False):
313 @todo Handle user notification better like attempting to login and failed login
315 @note This must be run outside of the UI lock
318 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
319 assert self._initDone, "Attempting login before app is fully loaded"
321 serviceId = self.NULL_BACKEND
325 self.refresh_session()
326 serviceId = self._defaultBackendId
328 except StandardError, e:
329 warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
332 loggedIn, serviceId = self._login_by_user(numOfAttempts)
334 with gtk_toolbox.gtk_lock():
335 self._change_loggedin_status(serviceId)
336 except StandardError, e:
337 with gtk_toolbox.gtk_lock():
338 self._errorDisplay.push_exception()
340 def _spawn_attempt_login(self, *args):
341 self._loginSink.send(args)
343 def refresh_session(self):
345 @note Thread agnostic
347 assert self._initDone, "Attempting login before app is fully loaded"
351 loggedIn = self._login_by_cookie()
353 loggedIn = self._login_by_settings()
356 raise RuntimeError("Login Failed")
358 def _login_by_cookie(self):
360 @note Thread agnostic
362 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
365 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
370 def _login_by_settings(self):
372 @note Thread agnostic
374 username, password = self._credentials
375 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
377 self._credentials = username, password
379 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
384 def _login_by_user(self, numOfAttempts):
386 @note This must be run outside of the UI lock
388 loggedIn, (username, password) = False, self._credentials
389 tmpServiceId = self.NULL_BACKEND
390 for attemptCount in xrange(numOfAttempts):
393 with gtk_toolbox.gtk_lock():
394 credentials = self._credentialsDialog.request_credentials(
395 defaultCredentials = self._credentials
397 tmpServiceId, username, password = credentials
398 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
401 serviceId = tmpServiceId
402 self._credentials = username, password
404 "Logged into %r through user request" % self._phoneBackends[serviceId],
408 serviceId = self.NULL_BACKEND
410 return loggedIn, serviceId
412 def _select_action(self, action, number, message):
413 self.refresh_session()
414 if action == "select":
415 self._dialpads[self._selectedBackendId].set_number(number)
416 self._notebook.set_current_page(self.KEYPAD_TAB)
417 elif action == "dial":
418 self._on_dial_clicked(number)
419 elif action == "sms":
420 self._on_sms_clicked(number, message)
422 assert False, "Unknown action: %s" % action
424 def _change_loggedin_status(self, newStatus):
425 oldStatus = self._selectedBackendId
426 if oldStatus == newStatus:
429 self._dialpads[oldStatus].disable()
430 self._accountViews[oldStatus].disable()
431 self._recentViews[oldStatus].disable()
432 self._messagesViews[oldStatus].disable()
433 self._contactsViews[oldStatus].disable()
435 self._dialpads[newStatus].enable()
436 self._accountViews[newStatus].enable()
437 self._recentViews[newStatus].enable()
438 self._messagesViews[newStatus].enable()
439 self._contactsViews[newStatus].enable()
441 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
442 self._phoneBackends[self._selectedBackendId].set_sane_callback()
443 self._accountViews[self._selectedBackendId].update()
445 self._selectedBackendId = newStatus
447 def load_settings(self, config):
452 self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
454 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
455 for i in xrange(len(self._credentials))
458 base64.b64decode(blob)
461 self._credentials = tuple(creds)
463 if self._alarmHandler is not None:
464 self._alarmHandler.load_settings(config, "alarm")
465 except ConfigParser.NoOptionError, e:
467 "Settings file %s is missing section %s" % (
468 constants._user_settings_,
473 except ConfigParser.NoSectionError, e:
475 "Settings file %s is missing section %s" % (
476 constants._user_settings_,
482 for backendId, view in itertools.chain(
483 self._dialpads.iteritems(),
484 self._accountViews.iteritems(),
485 self._messagesViews.iteritems(),
486 self._recentViews.iteritems(),
487 self._contactsViews.iteritems(),
489 sectionName = "%s - %s" % (backendId, view.name())
491 view.load_settings(config, sectionName)
492 except ConfigParser.NoOptionError, e:
494 "Settings file %s is missing section %s" % (
495 constants._user_settings_,
500 except ConfigParser.NoSectionError, e:
502 "Settings file %s is missing section %s" % (
503 constants._user_settings_,
509 def save_settings(self, config):
511 @note Thread Agnostic
513 config.add_section(constants.__pretty_app_name__)
514 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
515 for i, value in enumerate(self._credentials):
516 blob = base64.b64encode(value)
517 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
518 config.add_section("alarm")
519 if self._alarmHandler is not None:
520 self._alarmHandler.save_settings(config, "alarm")
522 for backendId, view in itertools.chain(
523 self._dialpads.iteritems(),
524 self._accountViews.iteritems(),
525 self._messagesViews.iteritems(),
526 self._recentViews.iteritems(),
527 self._contactsViews.iteritems(),
529 sectionName = "%s - %s" % (backendId, view.name())
530 config.add_section(sectionName)
531 view.save_settings(config, sectionName)
533 def _save_settings(self):
535 @note Thread Agnostic
537 config = ConfigParser.SafeConfigParser()
538 self.save_settings(config)
539 with open(constants._user_settings_, "wb") as configFile:
540 config.write(configFile)
542 def _refresh_active_tab(self):
543 pageIndex = self._notebook.get_current_page()
544 if pageIndex == self.CONTACTS_TAB:
545 self._contactsViews[self._selectedBackendId].update(force=True)
546 elif pageIndex == self.RECENT_TAB:
547 self._recentViews[self._selectedBackendId].update(force=True)
548 elif pageIndex == self.MESSAGES_TAB:
549 self._messagesViews[self._selectedBackendId].update(force=True)
551 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
552 if self._ledHandler is not None:
553 self._ledHandler.off()
555 def _on_close(self, *args, **kwds):
557 if self._osso is not None:
561 self._save_settings()
565 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
567 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
568 For system_inactivity, we have no background tasks to pause
570 @note Hildon specific
573 for backendId in self.BACKENDS:
574 self._phoneBackends[backendId].clear_caches()
575 self._contactsViews[self._selectedBackendId].clear_caches()
578 if save_unsaved_data or shutdown:
579 self._save_settings()
581 def _on_connection_change(self, connection, event, magicIdentifier):
583 @note Hildon specific
587 status = event.get_status()
588 error = event.get_error()
589 iap_id = event.get_iap_id()
590 bearer = event.get_bearer_type()
592 if status == conic.STATUS_CONNECTED:
594 self._spawn_attempt_login(2)
595 elif status == conic.STATUS_DISCONNECTED:
597 self._defaultBackendId = self._selectedBackendId
598 self._change_loggedin_status(self.NULL_BACKEND)
600 def _on_window_state_change(self, widget, event, *args):
602 @note Hildon specific
604 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
605 self._isFullScreen = True
607 self._isFullScreen = False
609 def _on_key_press(self, widget, event, *args):
611 @note Hildon specific
613 if event.keyval == gtk.keysyms.F6:
614 if self._isFullScreen:
615 self._window.unfullscreen()
617 self._window.fullscreen()
619 def _on_clearcookies_clicked(self, *args):
620 self._phoneBackends[self._selectedBackendId].logout()
621 self._accountViews[self._selectedBackendId].clear()
622 self._recentViews[self._selectedBackendId].clear()
623 self._messagesViews[self._selectedBackendId].clear()
624 self._contactsViews[self._selectedBackendId].clear()
625 self._change_loggedin_status(self.NULL_BACKEND)
627 self._spawn_attempt_login(2, True)
629 def _on_notebook_switch_page(self, notebook, page, pageIndex):
630 self._reset_tab_refresh()
632 didRecentUpdate = False
633 didMessagesUpdate = False
635 if pageIndex == self.RECENT_TAB:
636 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
637 elif pageIndex == self.MESSAGES_TAB:
638 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
639 elif pageIndex == self.CONTACTS_TAB:
640 self._contactsViews[self._selectedBackendId].update()
641 elif pageIndex == self.ACCOUNT_TAB:
642 self._accountViews[self._selectedBackendId].update()
644 if didRecentUpdate or didMessagesUpdate:
645 if self._ledHandler is not None:
646 self._ledHandler.off()
648 def _set_tab_refresh(self, *args):
649 pageIndex = self._notebook.get_current_page()
650 child = self._notebook.get_nth_page(pageIndex)
651 self._notebook.get_tab_label(child).set_text("Refresh?")
654 def _reset_tab_refresh(self, *args):
655 pageIndex = self._notebook.get_current_page()
656 child = self._notebook.get_nth_page(pageIndex)
657 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
660 def _on_tab_refresh(self, *args):
661 self._refresh_active_tab()
662 self._reset_tab_refresh()
665 def _on_sms_clicked(self, number, message):
666 assert number, "No number specified"
667 assert message, "Empty message"
669 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
670 except StandardError, e:
672 self._errorDisplay.push_exception()
676 self._errorDisplay.push_message(
677 "Backend link with grandcentral is not working, please try again"
683 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
685 except StandardError, e:
686 self._errorDisplay.push_exception()
687 except ValueError, e:
688 self._errorDisplay.push_exception()
691 self._dialpads[self._selectedBackendId].clear()
693 def _on_dial_clicked(self, number):
694 assert number, "No number to call"
696 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
697 except StandardError, e:
699 self._errorDisplay.push_exception()
703 self._errorDisplay.push_message(
704 "Backend link with grandcentral is not working, please try again"
710 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
711 self._phoneBackends[self._selectedBackendId].dial(number)
713 except StandardError, e:
714 self._errorDisplay.push_exception()
715 except ValueError, e:
716 self._errorDisplay.push_exception()
719 self._dialpads[self._selectedBackendId].clear()
721 def _on_menu_refresh(self, *args):
722 self._refresh_active_tab()
724 def _on_paste(self, *args):
725 contents = self._clipboard.wait_for_text()
726 self._dialpads[self._selectedBackendId].set_number(contents)
728 def _on_about_activate(self, *args):
729 dlg = gtk.AboutDialog()
730 dlg.set_name(constants.__pretty_app_name__)
731 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
732 dlg.set_copyright("Copyright 2008 - LGPL")
733 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")
734 dlg.set_website("http://gc-dialer.garage.maemo.org/")
735 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
743 failureCount, testCount = doctest.testmod()
745 print "Tests Successful"
752 _lock_file = os.path.join(constants._data_path_, ".lock")
753 #with gtk_toolbox.flock(_lock_file, 0):
754 gtk.gdk.threads_init()
756 if hildonize.IS_HILDON:
757 gtk.set_application_name(constants.__pretty_app_name__)
758 handle = Dialcentral()
762 class DummyOptions(object):
768 if __name__ == "__main__":
769 if len(sys.argv) > 1:
775 if optparse is not None:
776 parser = optparse.OptionParser()
777 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
778 (commandOptions, commandArgs) = parser.parse_args()
780 commandOptions = DummyOptions()
783 if commandOptions.test: