--- /dev/null
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import sys
+
+
+sys.path.append("/opt/dialcentral/lib")
+
+
+import dialcentral_qt
+
+
+if __name__ == "__main__":
+ dialcentral_qt.run()
+++ /dev/null
-Building a package
-===================
-Run
- make PLATFORM=... package
-which will create a "./pkg-.../..." heirarchy. Move this structure to somewhere on the tablet, then run pypackager.
-
-Supported PLATFORMs include
- desktop
- os2007
- os2008
-
-SDK Enviroment
-===================
-
-Native
-
-Follow install instructions
- Ubuntu: http://www.linuxuk.org/node/38
-Install Nokia stuff (for each target)
- fakeroot apt-get install maemo-explicit
-
-Userful commands
-Login
- /scratchbox/login
-Change targets
- sb-conf select DIABLO_ARMEL
- sb-conf select DIABLO_X86
-Fixing it
- fakeroot apt-get -f install
-
-Starting scratchbox
- Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac -extension Composite
- scratchbox
- export DISPLAY=:2
- af-sb-init.sh start
-Then running a command in the "Maemo" terminal will launch it in the Xephyr session
- Tip: run with "run-standalone.sh" for niceness?
+++ /dev/null
-http://www.gentleface.com/free_icon_set.html
-The Creative Commons Attribution-NonCommercial -- FREE
-http://creativecommons.org/licenses/by-nc-nd/3.0/
-
-Sound:
-http://www.freesound.org/samplesViewSingle.php?id=2166
-http://creativecommons.org/licenses/sampling+/1.0/
-
-placed.png, received.png, placed.png
-Free for commercial use
-http://www.iconeden.com/icon/free/get/bright-free-stock-iconset
--- /dev/null
+http://www.gentleface.com/free_icon_set.html
+The Creative Commons Attribution-NonCommercial -- FREE
+http://creativecommons.org/licenses/by-nc-nd/3.0/
+
+Sound:
+http://www.freesound.org/samplesViewSingle.php?id=2166
+http://creativecommons.org/licenses/sampling+/1.0/
+
+placed.png, received.png, placed.png
+Free for commercial use
+http://www.iconeden.com/icon/free/get/bright-free-stock-iconset
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="128pt"
+ height="128pt"
+ viewBox="0 0 128 128"
+ version="1.1"
+ id="svg2"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="dialcentral-base.svg">
+ <metadata
+ id="metadata26">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs24">
+ <linearGradient
+ id="linearGradient3805">
+ <stop
+ style="stop-color:#acdd6e;stop-opacity:1;"
+ offset="0"
+ id="stop3807" />
+ <stop
+ style="stop-color:#2f720c;stop-opacity:1;"
+ offset="1"
+ id="stop3809" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3787">
+ <stop
+ style="stop-color:#fbfbfb;stop-opacity:1;"
+ offset="0"
+ id="stop3789" />
+ <stop
+ style="stop-color:#fbfbfb;stop-opacity:0;"
+ offset="1"
+ id="stop3791" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3779">
+ <stop
+ style="stop-color:#fbfbfb;stop-opacity:1;"
+ offset="0"
+ id="stop3781" />
+ <stop
+ style="stop-color:#f6f6f6;stop-opacity:1;"
+ offset="1"
+ id="stop3783" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3771">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3773" />
+ <stop
+ style="stop-color:#f1f1f1;stop-opacity:1;"
+ offset="1"
+ id="stop3775" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3771"
+ id="linearGradient3802"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="scale(1.25,1.25)"
+ x1="10.26"
+ y1="30.534513"
+ x2="118.7"
+ y2="30.534513" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3805"
+ id="linearGradient3811"
+ x1="57.842336"
+ y1="48.798807"
+ x2="72.206342"
+ y2="48.798807"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3805"
+ id="linearGradient3819"
+ x1="58.040001"
+ y1="83.000084"
+ x2="71.960167"
+ y2="83.000084"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1176"
+ id="namedview22"
+ showgrid="false"
+ inkscape:zoom="3.6"
+ inkscape:cx="60.941186"
+ inkscape:cy="98.385892"
+ inkscape:window-x="0"
+ inkscape:window-y="24"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+ <path
+ fill="#c6c6c6"
+ d=" M 27.64 22.78 C 43.74 11.36 65.26 8.46 84.02 14.31 C 98.03 18.53 111.02 27.95 117.18 41.51 C 126.19 60.23 118.29 84.06 101.48 95.55 C 97.45 98.54 92.91 100.73 88.35 102.77 C 88.14 107.99 88.76 113.45 86.50 118.33 C 80.46 115.90 76.13 110.76 70.41 107.83 C 62.51 106.56 54.28 107.18 46.58 104.67 C 34.37 101.14 22.66 94.09 15.66 83.24 C 8.87 73.38 6.41 60.68 9.17 49.02 C 11.84 38.40 18.67 28.99 27.64 22.78 Z"
+ id="path6"
+ style="fill:#606060;fill-opacity:1" />
+ <path
+ style="fill:url(#linearGradient3802);fill-opacity:1"
+ d="M 80.9375,15.03125 C 66.292503,14.983928 51.629297,18.840625 39.09375,26.5 26.44375,34.125 16.1625,46.35 12.8125,60.9375 l -0.40625,0 c -1.05,6.325 -1.54375,12.8125 -0.21875,19.125 1.8875,8.7 5.95,16.73125 11.5,23.65625 8.65,9.9125 20.18125,17.14375 32.78125,20.90625 9.9625,3.3125 20.53125,3.6375 30.90625,4.125 7.7375,4.175 14.38125,10.225 21.65625,15.1875 0.2375,-6.725 -0.56875,-13.4625 -0.21875,-20.1875 2.3125,-1.5375 4.91875,-2.5625 7.40625,-3.75 8.175,-3.7625 15.25,-9.55 21.25,-16.1875 5.575,-6.9625 9.69375,-15.04375 11.59375,-23.78125 1.275,-6.35 0.80625,-12.81875 -0.21875,-19.15625 l 0.0174,-0.03472 C 144.54861,41.752778 128.0875,27.4125 110.3125,20.625 100.98398,16.933203 90.957761,15.063628 80.9375,15.03125 z m 0.15625,16.65625 c 4.188721,-0.13501 8.539062,2.65625 9.03125,7.03125 0.6875,7.45 -1.4875,14.79375 -2.3125,22.15625 -0.9375,6.3875 -1.8875,12.75625 -2.75,19.15625 -1.15,3.3625 0.2875,10.9125 -4.875,10.25 -2.025,-2.9875 -1.975,-6.825 -2.625,-10.25 -0.825,-6.2625 -1.775,-12.46875 -2.6875,-18.71875 -0.8625,-7.075 -2.8125,-14.10625 -2.5625,-21.28125 -0.0125,-3.9625 3.05,-7.56875 7,-8.09375 0.579688,-0.142187 1.182861,-0.230713 1.78125,-0.25 z m -0.71875,63.25 c 0.297791,-0.01522 0.589844,-0.025 0.90625,0 5.0375,-0.3375 7.68125,4.6 8.65625,8.8125 -0.975,4.225 -3.64375,9.1875 -8.71875,8.8125 -5.0375,0.3125 -7.81875,-4.575 -8.65625,-8.875 0.9375,-3.949219 3.345642,-8.521713 7.8125,-8.75 z"
+ transform="scale(0.8,0.8)"
+ id="path8"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="scccccccccccccccssccccccccssccccs" />
+ <path
+ fill="#636363"
+ d=" M 63.44 25.54 C 67.15 24.63 71.65 26.98 72.10 30.98 C 72.65 36.94 70.92 42.80 70.26 48.69 C 69.51 53.80 68.73 58.90 68.04 64.02 C 67.12 66.71 68.28 72.75 64.15 72.22 C 62.53 69.83 62.57 66.77 62.05 64.03 C 61.39 59.02 60.63 54.04 59.90 49.04 C 59.21 43.38 57.66 37.77 57.86 32.03 C 57.85 28.86 60.28 25.96 63.44 25.54 Z"
+ id="path10"
+ style="fill:url(#linearGradient3811);fill-opacity:1" />
+ <path
+ d="m 58.04,82.96 c 0.05,-3.453333 2.94,-6.978889 6.99,-7.02 4.113333,0.0078 6.927778,3.672222 6.93,7.07 0.02556,3.546667 -2.836667,7.016667 -6.98,7.05 -4.057778,0.02778 -6.936667,-3.493333 -6.94,-7.1 z"
+ id="path18"
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient3819);fill-opacity:1.0"
+ sodipodi:nodetypes="ccccc" />
+</svg>
--- /dev/null
+Your icon's dominant color is #72ab40
+A suggested disabled color is #d1ffa9
+A suggested pressed color is #57743e
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="80" width="80">
+ <defs>
+ <linearGradient id="hicg_overlay_grad" gradientUnits="userSpaceOnUse" x1="39.9995" y1="5.1816" x2="39.9995" y2="58.8019">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#000000"/>
+ </linearGradient>
+ <filter id="hicg_drop_shadow">
+ <feOffset in="SourceAlpha" dx="0" dy="4"/>
+ <feGaussianBlur stdDeviation="4"/>
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.5 0" result="shadow"/>
+ <feBlend in="SourceGraphic" in2="shadow" mode="normal"/>
+ </filter>
+ </defs>
+ <g>
+ <path id="hicg_background" fill="#72ab40" d="M79,40c0,28.893-10.105,39-39,39S1,68.893,1,40C1,11.106,11.105,1,40,1S79,11.106,79,40z"/>
+ <path id="hicg_highlight" fill="#fff" opacity="0.25" d="M39.999,1C11.105,1,1,11.106,1,40c0,28.893,10.105,39,38.999,39 C68.896,79,79,68.893,79,40C79,11.106,68.896,1,39.999,1z M39.999,78.025C11.57,78.025,1.976,68.43,1.976,40 c0-28.429,9.595-38.024,38.023-38.024c28.43,0,38.024,9.596,38.024,38.024C78.023,68.43,68.429,78.025,39.999,78.025z"/>
+ <path id="hicg_overlay" opacity="0.4" fill="url(#hicg_overlay_grad)" d="M78.977,40c0,28.893-10.1,39-38.977,39S1.023,68.893,1.023,40c0-28.894,10.1-39,38.977-39S78.977,11.106,78.977,40z"/>
+ </g>
+<g xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" transform="translate(12, 12) scale(0.4375)" filter="url(#hicg_drop_shadow)">
+ <metadata xmlns="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" id="metadata26">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" id="defs24">
+ <linearGradient id="linearGradient3805">
+ <stop style="stop-color:#acdd6e;stop-opacity:1;" offset="0" id="stop3807"/>
+ <stop style="stop-color:#2f720c;stop-opacity:1;" offset="1" id="stop3809"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3787">
+ <stop style="stop-color:#fbfbfb;stop-opacity:1;" offset="0" id="stop3789"/>
+ <stop style="stop-color:#fbfbfb;stop-opacity:0;" offset="1" id="stop3791"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3779">
+ <stop style="stop-color:#fbfbfb;stop-opacity:1;" offset="0" id="stop3781"/>
+ <stop style="stop-color:#f6f6f6;stop-opacity:1;" offset="1" id="stop3783"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3771">
+ <stop style="stop-color:#ffffff;stop-opacity:1;" offset="0" id="stop3773"/>
+ <stop style="stop-color:#f1f1f1;stop-opacity:1;" offset="1" id="stop3775"/>
+ </linearGradient>
+ <linearGradient inkscape:collect="always" xlink:href="#linearGradient3771" id="linearGradient3802" gradientUnits="userSpaceOnUse" gradientTransform="scale(1.25,1.25)" x1="10.26" y1="30.534513" x2="118.7" y2="30.534513"/>
+ <linearGradient inkscape:collect="always" xlink:href="#linearGradient3805" id="linearGradient3811" x1="57.842336" y1="48.798807" x2="72.206342" y2="48.798807" gradientUnits="userSpaceOnUse"/>
+ <linearGradient inkscape:collect="always" xlink:href="#linearGradient3805" id="linearGradient3819" x1="58.040001" y1="83.000084" x2="71.960167" y2="83.000084" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <sodipodi:namedview xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" inkscape:window-height="1176" id="namedview22" showgrid="false" inkscape:zoom="3.6" inkscape:cx="60.941186" inkscape:cy="98.385892" inkscape:window-x="0" inkscape:window-y="24" inkscape:window-maximized="1" inkscape:current-layer="svg2"/>
+ <path xmlns="http://www.w3.org/2000/svg" fill="#c6c6c6" d=" M 27.64 22.78 C 43.74 11.36 65.26 8.46 84.02 14.31 C 98.03 18.53 111.02 27.95 117.18 41.51 C 126.19 60.23 118.29 84.06 101.48 95.55 C 97.45 98.54 92.91 100.73 88.35 102.77 C 88.14 107.99 88.76 113.45 86.50 118.33 C 80.46 115.90 76.13 110.76 70.41 107.83 C 62.51 106.56 54.28 107.18 46.58 104.67 C 34.37 101.14 22.66 94.09 15.66 83.24 C 8.87 73.38 6.41 60.68 9.17 49.02 C 11.84 38.40 18.67 28.99 27.64 22.78 Z" id="path6" style="fill:#606060;fill-opacity:1"/>
+ <path xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" style="fill:url(#linearGradient3802);fill-opacity:1" d="M 80.9375,15.03125 C 66.292503,14.983928 51.629297,18.840625 39.09375,26.5 26.44375,34.125 16.1625,46.35 12.8125,60.9375 l -0.40625,0 c -1.05,6.325 -1.54375,12.8125 -0.21875,19.125 1.8875,8.7 5.95,16.73125 11.5,23.65625 8.65,9.9125 20.18125,17.14375 32.78125,20.90625 9.9625,3.3125 20.53125,3.6375 30.90625,4.125 7.7375,4.175 14.38125,10.225 21.65625,15.1875 0.2375,-6.725 -0.56875,-13.4625 -0.21875,-20.1875 2.3125,-1.5375 4.91875,-2.5625 7.40625,-3.75 8.175,-3.7625 15.25,-9.55 21.25,-16.1875 5.575,-6.9625 9.69375,-15.04375 11.59375,-23.78125 1.275,-6.35 0.80625,-12.81875 -0.21875,-19.15625 l 0.0174,-0.03472 C 144.54861,41.752778 128.0875,27.4125 110.3125,20.625 100.98398,16.933203 90.957761,15.063628 80.9375,15.03125 z m 0.15625,16.65625 c 4.188721,-0.13501 8.539062,2.65625 9.03125,7.03125 0.6875,7.45 -1.4875,14.79375 -2.3125,22.15625 -0.9375,6.3875 -1.8875,12.75625 -2.75,19.15625 -1.15,3.3625 0.2875,10.9125 -4.875,10.25 -2.025,-2.9875 -1.975,-6.825 -2.625,-10.25 -0.825,-6.2625 -1.775,-12.46875 -2.6875,-18.71875 -0.8625,-7.075 -2.8125,-14.10625 -2.5625,-21.28125 -0.0125,-3.9625 3.05,-7.56875 7,-8.09375 0.579688,-0.142187 1.182861,-0.230713 1.78125,-0.25 z m -0.71875,63.25 c 0.297791,-0.01522 0.589844,-0.025 0.90625,0 5.0375,-0.3375 7.68125,4.6 8.65625,8.8125 -0.975,4.225 -3.64375,9.1875 -8.71875,8.8125 -5.0375,0.3125 -7.81875,-4.575 -8.65625,-8.875 0.9375,-3.949219 3.345642,-8.521713 7.8125,-8.75 z" transform="scale(0.8,0.8)" id="path8" inkscape:connector-curvature="0" sodipodi:nodetypes="scccccccccccccccssccccccccssccccs"/>
+ <path xmlns="http://www.w3.org/2000/svg" fill="#636363" d=" M 63.44 25.54 C 67.15 24.63 71.65 26.98 72.10 30.98 C 72.65 36.94 70.92 42.80 70.26 48.69 C 69.51 53.80 68.73 58.90 68.04 64.02 C 67.12 66.71 68.28 72.75 64.15 72.22 C 62.53 69.83 62.57 66.77 62.05 64.03 C 61.39 59.02 60.63 54.04 59.90 49.04 C 59.21 43.38 57.66 37.77 57.86 32.03 C 57.85 28.86 60.28 25.96 63.44 25.54 Z" id="path10" style="fill:url(#linearGradient3811);fill-opacity:1"/>
+ <path xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" d="m 58.04,82.96 c 0.05,-3.453333 2.94,-6.978889 6.99,-7.02 4.113333,0.0078 6.927778,3.672222 6.93,7.07 0.02556,3.546667 -2.836667,7.016667 -6.98,7.05 -4.057778,0.02778 -6.936667,-3.493333 -6.94,-7.1 z" id="path18" inkscape:connector-curvature="0" style="fill:url(#linearGradient3819);fill-opacity:1.0" sodipodi:nodetypes="ccccc"/>
+</g></svg>
--- /dev/null
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Name=DialCentral
+Exec=/usr/bin/run-standalone.sh /opt/dialcentral/bin/dialcentral.py
+Icon=dialcentral
+Categories=Network;InstantMessaging;Qt;
--- /dev/null
+#!/usr/bin/env python
--- /dev/null
+#!/usr/bin/env python
+
+import os
+import time
+import datetime
+import ConfigParser
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+import dbus
+
+
+_FREMANTLE_ALARM = "Fremantle"
+_DIABLO_ALARM = "Diablo"
+_NO_ALARM = "None"
+
+
+try:
+ import alarm
+ ALARM_TYPE = _FREMANTLE_ALARM
+except (ImportError, OSError):
+ try:
+ import osso.alarmd as alarmd
+ ALARM_TYPE = _DIABLO_ALARM
+ except (ImportError, OSError):
+ ALARM_TYPE = _NO_ALARM
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+def _get_start_time(recurrence):
+ now = datetime.datetime.now()
+ startTimeMinute = now.minute + max(recurrence, 5) # being safe
+ startTimeHour = now.hour + int(startTimeMinute / 60)
+ startTimeMinute = startTimeMinute % 59
+ now.replace(minute=startTimeMinute)
+ timestamp = int(time.mktime(now.timetuple()))
+ return timestamp
+
+
+def _create_recurrence_mask(recurrence, base):
+ """
+ >>> bin(_create_recurrence_mask(60, 60))
+ '0b1'
+ >>> bin(_create_recurrence_mask(30, 60))
+ '0b1000000000000000000000000000001'
+ >>> bin(_create_recurrence_mask(2, 60))
+ '0b10101010101010101010101010101010101010101010101010101010101'
+ >>> bin(_create_recurrence_mask(1, 60))
+ '0b111111111111111111111111111111111111111111111111111111111111'
+ """
+ mask = 0
+ for i in xrange(base / recurrence):
+ mask |= 1 << (recurrence * i)
+ return mask
+
+
+def _unpack_minutes(recurrence):
+ """
+ >>> _unpack_minutes(0)
+ (0, 0, 0)
+ >>> _unpack_minutes(1)
+ (0, 0, 1)
+ >>> _unpack_minutes(59)
+ (0, 0, 59)
+ >>> _unpack_minutes(60)
+ (0, 1, 0)
+ >>> _unpack_minutes(129)
+ (0, 2, 9)
+ >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2)
+ (5, 3, 2)
+ >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2)
+ (5, 3, 2)
+ """
+ minutesInAnHour = 60
+ minutesInDay = 24 * minutesInAnHour
+ minutesInAWeek = minutesInDay * 7
+
+ days = recurrence / minutesInDay
+ daysOfWeek = days % 7
+ recurrence -= days * minutesInDay
+ hours = recurrence / minutesInAnHour
+ recurrence -= hours * minutesInAnHour
+ mins = recurrence % minutesInAnHour
+ recurrence -= mins
+ assert recurrence == 0, "Recurrence %d" % recurrence
+ return daysOfWeek, hours, mins
+
+
+class _FremantleAlarmHandler(object):
+
+ _INVALID_COOKIE = -1
+ _REPEAT_FOREVER = -1
+ _TITLE = "Dialcentral Notifications"
+ _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+
+ def __init__(self):
+ self._recurrence = 5
+
+ self._alarmCookie = self._INVALID_COOKIE
+ self._launcher = self._LAUNCHER
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._recurrence = config.getint(sectionName, "recurrence")
+ self._alarmCookie = config.getint(sectionName, "alarmCookie")
+ launcher = config.get(sectionName, "notifier")
+ if launcher:
+ self._launcher = launcher
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def save_settings(self, config, sectionName):
+ try:
+ config.set(sectionName, "recurrence", str(self._recurrence))
+ config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+ launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+ config.set(sectionName, "notifier", launcher)
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def apply_settings(self, enabled, recurrence):
+ if recurrence != self._recurrence or enabled != self.isEnabled:
+ if self.isEnabled:
+ self._clear_alarm()
+ if enabled:
+ self._set_alarm(recurrence)
+ self._recurrence = int(recurrence)
+
+ @property
+ def recurrence(self):
+ return self._recurrence
+
+ @property
+ def isEnabled(self):
+ return self._alarmCookie != self._INVALID_COOKIE
+
+ def _set_alarm(self, recurrenceMins):
+ assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins
+ alarmTime = _get_start_time(recurrenceMins)
+
+ event = alarm.Event()
+ event.appid = self._TITLE
+ event.alarm_time = alarmTime
+ event.recurrences_left = self._REPEAT_FOREVER
+
+ action = event.add_actions(1)[0]
+ action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED
+ action.command = self._launcher
+
+ recurrence = event.add_recurrences(1)[0]
+ recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60)
+ recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE
+ recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE
+ recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE
+ recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE
+ recurrence.special |= alarm.RECUR_SPECIAL_NONE
+
+ assert event.is_sane()
+ self._alarmCookie = alarm.add_event(event)
+
+ def _clear_alarm(self):
+ if self._alarmCookie == self._INVALID_COOKIE:
+ return
+ alarm.delete_event(self._alarmCookie)
+ self._alarmCookie = self._INVALID_COOKIE
+
+
+class _DiabloAlarmHandler(object):
+
+ _INVALID_COOKIE = -1
+ _TITLE = "Dialcentral Notifications"
+ _LAUNCHER = os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
+ _REPEAT_FOREVER = -1
+
+ def __init__(self):
+ self._recurrence = 5
+
+ bus = dbus.SystemBus()
+ self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
+ self._alarmCookie = self._INVALID_COOKIE
+ self._launcher = self._LAUNCHER
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._recurrence = config.getint(sectionName, "recurrence")
+ self._alarmCookie = config.getint(sectionName, "alarmCookie")
+ launcher = config.get(sectionName, "notifier")
+ if launcher:
+ self._launcher = launcher
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "recurrence", str(self._recurrence))
+ config.set(sectionName, "alarmCookie", str(self._alarmCookie))
+ launcher = self._launcher if self._launcher != self._LAUNCHER else ""
+ config.set(sectionName, "notifier", launcher)
+
+ def apply_settings(self, enabled, recurrence):
+ if recurrence != self._recurrence or enabled != self.isEnabled:
+ if self.isEnabled:
+ self._clear_alarm()
+ if enabled:
+ self._set_alarm(recurrence)
+ self._recurrence = int(recurrence)
+
+ @property
+ def recurrence(self):
+ return self._recurrence
+
+ @property
+ def isEnabled(self):
+ return self._alarmCookie != self._INVALID_COOKIE
+
+ def _set_alarm(self, recurrence):
+ assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence
+ alarmTime = _get_start_time(recurrence)
+
+ #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
+ _DEFAULT_FLAGS = (
+ alarmd.ALARM_EVENT_NO_DIALOG |
+ alarmd.ALARM_EVENT_NO_SNOOZE |
+ alarmd.ALARM_EVENT_CONNECTED
+ )
+ action = []
+ action.extend(['flags', _DEFAULT_FLAGS])
+ action.extend(['title', self._TITLE])
+ action.extend(['path', self._launcher])
+ action.extend([
+ 'arguments',
+ dbus.Array(
+ [alarmTime, int(27)],
+ signature=dbus.Signature('v')
+ )
+ ]) #int(27) used in place of alarm_index
+
+ event = []
+ event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
+ event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')]) #use AlarmdActionExec instead of AlarmdActionDbus
+ event.append(dbus.UInt32(len(action) / 2))
+ event.extend(action)
+ event.extend(['time', dbus.Int64(alarmTime)])
+ event.extend(['recurr_interval', dbus.UInt32(recurrence)])
+ event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
+
+ self._alarmCookie = self._alarmdDBus.add_event(*event);
+
+ def _clear_alarm(self):
+ if self._alarmCookie == self._INVALID_COOKIE:
+ return
+ deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
+ self._alarmCookie = self._INVALID_COOKIE
+ assert deleteResult != -1, "Deleting of alarm event failed"
+
+
+class _ApplicationAlarmHandler(object):
+
+ _REPEAT_FOREVER = -1
+ _MIN_TO_MS_FACTORY = 1000 * 60
+
+ def __init__(self):
+ self._timer = QtCore.QTimer()
+ self._timer.setSingleShot(False)
+ self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY)
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY)
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+ self._timer.start()
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "recurrence", str(self.recurrence))
+
+ def apply_settings(self, enabled, recurrence):
+ self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY)
+ if enabled:
+ self._timer.start()
+ else:
+ self._timer.stop()
+
+ @property
+ def notifySignal(self):
+ return self._timer.timeout
+
+ @property
+ def recurrence(self):
+ return int(self._timer.interval() / self._MIN_TO_MS_FACTORY)
+
+ @property
+ def isEnabled(self):
+ return self._timer.isActive()
+
+
+class _NoneAlarmHandler(object):
+
+ def __init__(self):
+ self._enabled = False
+ self._recurrence = 5
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._recurrence = config.getint(sectionName, "recurrence")
+ self._enabled = True
+ except ConfigParser.NoOptionError:
+ pass
+ except ConfigParser.NoSectionError:
+ pass
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "recurrence", str(self.recurrence))
+
+ def apply_settings(self, enabled, recurrence):
+ self._enabled = enabled
+
+ @property
+ def recurrence(self):
+ return self._recurrence
+
+ @property
+ def isEnabled(self):
+ return self._enabled
+
+
+_BACKGROUND_ALARM_FACTORY = {
+ _FREMANTLE_ALARM: _FremantleAlarmHandler,
+ _DIABLO_ALARM: _DiabloAlarmHandler,
+ _NO_ALARM: None,
+}[ALARM_TYPE]
+
+
+class AlarmHandler(object):
+
+ ALARM_NONE = "No Alert"
+ ALARM_BACKGROUND = "Background Alert"
+ ALARM_APPLICATION = "Application Alert"
+ ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION]
+
+ ALARM_FACTORY = {
+ ALARM_NONE: _NoneAlarmHandler,
+ ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY,
+ ALARM_APPLICATION: _ApplicationAlarmHandler,
+ }
+
+ def __init__(self):
+ self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()}
+ self._currentAlarmType = self.ALARM_NONE
+
+ def load_settings(self, config, sectionName):
+ try:
+ self._currentAlarmType = config.get(sectionName, "alarm")
+ except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+ _moduleLogger.exception("Falling back to old style")
+ self._currentAlarmType = self.ALARM_BACKGROUND
+ if self._currentAlarmType not in self.ALARM_TYPES:
+ self._currentAlarmType = self.ALARM_NONE
+
+ self._init_alarm(self._currentAlarmType)
+ if self._currentAlarmType in self._alarms:
+ self._alarms[self._currentAlarmType].load_settings(config, sectionName)
+ if not self._alarms[self._currentAlarmType].isEnabled:
+ _moduleLogger.info("Config file lied, not actually enabled")
+ self._currentAlarmType = self.ALARM_NONE
+ else:
+ _moduleLogger.info("Background alerts not supported")
+ self._currentAlarmType = self.ALARM_NONE
+
+ def save_settings(self, config, sectionName):
+ config.set(sectionName, "alarm", self._currentAlarmType)
+ self._alarms[self._currentAlarmType].save_settings(config, sectionName)
+
+ def apply_settings(self, t, recurrence):
+ self._init_alarm(t)
+ newHandler = self._alarms[t]
+ oldHandler = self._alarms[self._currentAlarmType]
+ if newHandler != oldHandler:
+ oldHandler.apply_settings(False, 0)
+ newHandler.apply_settings(True, recurrence)
+ self._currentAlarmType = t
+
+ @property
+ def alarmType(self):
+ return self._currentAlarmType
+
+ @property
+ def backgroundNotificationsSupported(self):
+ return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None
+
+ @property
+ def applicationNotifySignal(self):
+ self._init_alarm(self.ALARM_APPLICATION)
+ return self._alarms[self.ALARM_APPLICATION].notifySignal
+
+ @property
+ def recurrence(self):
+ return self._alarms[self._currentAlarmType].recurrence
+
+ @property
+ def isEnabled(self):
+ return self._currentAlarmType != self.ALARM_NONE
+
+ def _init_alarm(self, t):
+ if t not in self._alarms and self.ALARM_FACTORY[t] is not None:
+ self._alarms[t] = self.ALARM_FACTORY[t]()
+
+
+def main():
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, format=logFormat)
+ import constants
+ try:
+ import optparse
+ except ImportError:
+ return
+
+ parser = optparse.OptionParser()
+ parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
+ parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+ parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
+ parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
+ (commandOptions, commandArgs) = parser.parse_args()
+
+ alarmHandler = AlarmHandler()
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ alarmHandler.load_settings(config, "alarm")
+
+ if commandOptions.display:
+ print "Alarm (%s) is %s for every %d minutes" % (
+ alarmHandler._alarmCookie,
+ "enabled" if alarmHandler.isEnabled else "disabled",
+ alarmHandler.recurrence,
+ )
+ else:
+ isEnabled = commandOptions.enabled
+ recurrence = commandOptions.recurrence
+ alarmHandler.apply_settings(isEnabled, recurrence)
+
+ alarmHandler.save_settings(config, "alarm")
+ configFile = open(constants._user_settings_, "wb")
+ try:
+ config.write(configFile)
+ finally:
+ configFile.close()
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+#!/usr/bin/env python
+
+import os
+import filecmp
+import ConfigParser
+import pprint
+import logging
+import logging.handlers
+
+import constants
+from backends.gvoice import gvoice
+
+
+def get_missed(backend):
+ missedPage = backend._browser.download(backend._XML_MISSED_URL)
+ missedJson = backend._grab_json(missedPage)
+ return missedJson
+
+
+def get_voicemail(backend):
+ voicemailPage = backend._browser.download(backend._XML_VOICEMAIL_URL)
+ voicemailJson = backend._grab_json(voicemailPage)
+ return voicemailJson
+
+
+def get_sms(backend):
+ smsPage = backend._browser.download(backend._XML_SMS_URL)
+ smsJson = backend._grab_json(smsPage)
+ return smsJson
+
+
+def remove_reltime(data):
+ for messageData in data["messages"].itervalues():
+ for badPart in [
+ "relTime",
+ "relativeStartTime",
+ "time",
+ "star",
+ "isArchived",
+ "isRead",
+ "isSpam",
+ "isTrash",
+ "labels",
+ ]:
+ if badPart in messageData:
+ del messageData[badPart]
+ for globalBad in ["unreadCounts", "totalSize", "resultsPerPage"]:
+ if globalBad in data:
+ del data[globalBad]
+
+
+def is_type_changed(backend, type, get_material):
+ jsonMaterial = get_material(backend)
+ unreadCount = jsonMaterial["unreadCounts"][type]
+
+ previousSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.old.json" % type)
+ currentSnapshotPath = os.path.join(constants._data_path_, "snapshot_%s.json" % type)
+
+ try:
+ os.remove(previousSnapshotPath)
+ except OSError, e:
+ # check if failed purely because the old file didn't exist, which is fine
+ if e.errno != 2:
+ raise
+ try:
+ os.rename(currentSnapshotPath, previousSnapshotPath)
+ previousExists = True
+ except OSError, e:
+ # check if failed purely because the new old file didn't exist, which is fine
+ if e.errno != 2:
+ raise
+ previousExists = False
+
+ remove_reltime(jsonMaterial)
+ textMaterial = pprint.pformat(jsonMaterial)
+ currentSnapshot = file(currentSnapshotPath, "w")
+ try:
+ currentSnapshot.write(textMaterial)
+ finally:
+ currentSnapshot.close()
+
+ if unreadCount == 0 or not previousExists:
+ return False
+
+ seemEqual = filecmp.cmp(previousSnapshotPath, currentSnapshotPath)
+ return not seemEqual
+
+
+def create_backend(config):
+ gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
+ backend = gvoice.GVoiceBackend(gvCookiePath)
+
+ loggedIn = False
+
+ if not loggedIn:
+ loggedIn = backend.refresh_account_info() is not None
+
+ if not loggedIn:
+ import base64
+ try:
+ blobs = (
+ config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+ for i in xrange(2)
+ )
+ creds = (
+ base64.b64decode(blob)
+ for blob in blobs
+ )
+ username, password = tuple(creds)
+ loggedIn = backend.login(username, password) is not None
+ except ConfigParser.NoOptionError, e:
+ pass
+ except ConfigParser.NoSectionError, e:
+ pass
+
+ assert loggedIn
+ return backend
+
+
+def is_changed(config, backend):
+ try:
+ notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+ notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+ notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+ except ConfigParser.NoOptionError, e:
+ notifyOnMissed = False
+ notifyOnVoicemail = False
+ notifyOnSms = False
+ except ConfigParser.NoSectionError, e:
+ notifyOnMissed = False
+ notifyOnVoicemail = False
+ notifyOnSms = False
+ logging.debug(
+ "Missed: %s, Voicemail: %s, SMS: %s" % (notifyOnMissed, notifyOnVoicemail, notifyOnSms)
+ )
+
+ notifySources = []
+ if notifyOnMissed:
+ notifySources.append(("missed", get_missed))
+ if notifyOnVoicemail:
+ notifySources.append(("voicemail", get_voicemail))
+ if notifyOnSms:
+ notifySources.append(("sms", get_sms))
+
+ notifyUser = False
+ for type, get_material in notifySources:
+ if is_type_changed(backend, type, get_material):
+ notifyUser = True
+ return notifyUser
+
+
+def notify_on_change():
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ backend = create_backend(config)
+ notifyUser = is_changed(config, backend)
+
+ if notifyUser:
+ logging.info("Changed")
+ import led_handler
+ led = led_handler.LedHandler()
+ led.on()
+ else:
+ logging.info("No Change")
+
+
+if __name__ == "__main__":
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, format=logFormat)
+ rotating = logging.handlers.RotatingFileHandler(constants._notifier_logpath_, maxBytes=512*1024, backupCount=1)
+ rotating.setFormatter(logging.Formatter(logFormat))
+ root = logging.getLogger()
+ root.addHandler(rotating)
+ logging.info("Notifier %s-%s" % (constants.__version__, constants.__build__))
+ logging.info("OS: %s" % (os.uname()[0], ))
+ logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+ logging.info("Hostname: %s" % os.uname()[1])
+ try:
+ notify_on_change()
+ except:
+ logging.exception("Error")
+ raise
--- /dev/null
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Filesystem backend for contact support
+"""
+
+from __future__ import with_statement
+
+import os
+import csv
+
+
+def try_unicode(s):
+ try:
+ return s.decode("UTF-8")
+ except UnicodeDecodeError:
+ return s
+
+
+class CsvAddressBook(object):
+ """
+ Currently supported file format
+ @li Has the first line as a header
+ @li Escapes with quotes
+ @li Comma as delimiter
+ @li Column 0 is name, column 1 is number
+ """
+
+ def __init__(self, name, csvPath):
+ self._name = name
+ self._csvPath = csvPath
+ self._contacts = {}
+
+ @property
+ def name(self):
+ return self._name
+
+ def update_account(self, force = True):
+ if not force or not self._contacts:
+ return
+ self._contacts = dict(
+ self._read_csv(self._csvPath)
+ )
+
+ def get_contacts(self):
+ """
+ @returns Iterable of (contact id, contact name)
+ """
+ if not self._contacts:
+ self._contacts = dict(
+ self._read_csv(self._csvPath)
+ )
+ return self._contacts
+
+ def _read_csv(self, csvPath):
+ try:
+ f = open(csvPath, "rU")
+ csvReader = iter(csv.reader(f))
+ except IOError, e:
+ if e.errno == 2:
+ return
+ raise
+
+ header = csvReader.next()
+ nameColumns, nameFallbacks, phoneColumns = self._guess_columns(header)
+
+ yieldCount = 0
+ for row in csvReader:
+ contactDetails = []
+ for (phoneType, phoneColumn) in phoneColumns:
+ try:
+ if len(row[phoneColumn]) == 0:
+ continue
+ contactDetails.append({
+ "phoneType": try_unicode(phoneType),
+ "phoneNumber": row[phoneColumn],
+ })
+ except IndexError:
+ pass
+ if 0 < len(contactDetails):
+ nameParts = (row[i].strip() for i in nameColumns)
+ nameParts = (part for part in nameParts if part)
+ fullName = " ".join(nameParts).strip()
+ if not fullName:
+ for fallbackColumn in nameFallbacks:
+ if row[fallbackColumn].strip():
+ fullName = row[fallbackColumn].strip()
+ break
+ else:
+ fullName = "Unknown"
+ fullName = try_unicode(fullName)
+ yield str(yieldCount), {
+ "contactId": "%s-%d" % (self._name, yieldCount),
+ "name": fullName,
+ "numbers": contactDetails,
+ }
+ yieldCount += 1
+
+ @classmethod
+ def _guess_columns(cls, row):
+ firstMiddleLast = [-1, -1, -1]
+ names = []
+ nameFallbacks = []
+ phones = []
+ for i, item in enumerate(row):
+ lowerItem = item.lower()
+ if 0 <= lowerItem.find("name"):
+ names.append((item, i))
+
+ if 0 <= lowerItem.find("couple"):
+ names.insert(0, (item, i))
+
+ if 0 <= lowerItem.find("first") or 0 <= lowerItem.find("given"):
+ firstMiddleLast[0] = i
+ elif 0 <= lowerItem.find("middle"):
+ firstMiddleLast[1] = i
+ elif 0 <= lowerItem.find("last") or 0 <= lowerItem.find("family"):
+ firstMiddleLast[2] = i
+ elif 0 <= lowerItem.find("phone"):
+ phones.append((item, i))
+ elif 0 <= lowerItem.find("mobile"):
+ phones.append((item, i))
+ elif 0 <= lowerItem.find("email") or 0 <= lowerItem.find("e-mail"):
+ nameFallbacks.append(i)
+ if len(names) == 0:
+ names.append(("Name", 0))
+ if len(phones) == 0:
+ phones.append(("Phone", 1))
+
+ nameColumns = [i for i in firstMiddleLast if 0 <= i]
+ if len(nameColumns) < 2:
+ del nameColumns[:]
+ nameColumns.append(names[0][1])
+
+ return nameColumns, nameFallbacks, phones
+
+
+class FilesystemAddressBookFactory(object):
+
+ FILETYPE_SUPPORT = {
+ "csv": CsvAddressBook,
+ }
+
+ def __init__(self, path):
+ self._path = path
+
+ def get_addressbooks(self):
+ for root, dirs, filenames in os.walk(self._path):
+ for filename in filenames:
+ try:
+ name, ext = filename.rsplit(".", 1)
+ except ValueError:
+ continue
+
+ try:
+ cls = self.FILETYPE_SUPPORT[ext]
+ except KeyError:
+ continue
+ yield cls(name, os.path.join(root, filename))
--- /dev/null
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's GoogleVoice service.
+Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Google Voice backend code
+
+Resources
+ http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
+ http://posttopic.com/topic/google-voice-add-on-development
+"""
+
+from __future__ import with_statement
+
+import itertools
+import logging
+
+from gvoice import gvoice
+
+from util import io as io_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class GVDialer(object):
+
+ MESSAGE_TEXTS = "Text"
+ MESSAGE_VOICEMAILS = "Voicemail"
+ MESSAGE_ALL = "All"
+
+ HISTORY_RECEIVED = "Received"
+ HISTORY_MISSED = "Missed"
+ HISTORY_PLACED = "Placed"
+ HISTORY_ALL = "All"
+
+ def __init__(self, cookieFile = None):
+ self._gvoice = gvoice.GVoiceBackend(cookieFile)
+ self._texts = []
+ self._voicemails = []
+ self._received = []
+ self._missed = []
+ self._placed = []
+
+ def is_quick_login_possible(self):
+ """
+ @returns True then refresh_account_info might be enough to login, else full login is required
+ """
+ return self._gvoice.is_quick_login_possible()
+
+ def refresh_account_info(self):
+ return self._gvoice.refresh_account_info()
+
+ def login(self, username, password):
+ """
+ Attempt to login to GoogleVoice
+ @returns Whether login was successful or not
+ """
+ return self._gvoice.login(username, password)
+
+ def logout(self):
+ self._texts = []
+ self._voicemails = []
+ self._received = []
+ self._missed = []
+ self._placed = []
+ return self._gvoice.logout()
+
+ def persist(self):
+ return self._gvoice.persist()
+
+ def is_dnd(self):
+ return self._gvoice.is_dnd()
+
+ def set_dnd(self, doNotDisturb):
+ return self._gvoice.set_dnd(doNotDisturb)
+
+ def call(self, outgoingNumber):
+ """
+ This is the main function responsible for initating the callback
+ """
+ return self._gvoice.call(outgoingNumber)
+
+ def cancel(self, outgoingNumber=None):
+ """
+ Cancels a call matching outgoing and forwarding numbers (if given).
+ Will raise an error if no matching call is being placed
+ """
+ return self._gvoice.cancel(outgoingNumber)
+
+ def send_sms(self, phoneNumbers, message):
+ self._gvoice.send_sms(phoneNumbers, message)
+
+ def search(self, query):
+ """
+ Search your Google Voice Account history for calls, voicemails, and sms
+ Returns ``Folder`` instance containting matching messages
+ """
+ return self._gvoice.search(query)
+
+ def get_feed(self, feed):
+ return self._gvoice.get_feed(feed)
+
+ def download(self, messageId, targetPath):
+ """
+ Download a voicemail or recorded call MP3 matching the given ``msg``
+ which can either be a ``Message`` instance, or a SHA1 identifier.
+ Message hashes can be found in ``self.voicemail().messages`` for example.
+ Returns location of saved file.
+ """
+ self._gvoice.download(messageId, targetPath)
+
+ def is_valid_syntax(self, number):
+ """
+ @returns If This number be called ( syntax validation only )
+ """
+ return self._gvoice.is_valid_syntax(number)
+
+ def get_account_number(self):
+ """
+ @returns The GoogleVoice phone number
+ """
+ return self._gvoice.get_account_number()
+
+ def get_callback_numbers(self):
+ """
+ @returns a dictionary mapping call back numbers to descriptions
+ @note These results are cached for 30 minutes.
+ """
+ return self._gvoice.get_callback_numbers()
+
+ def set_callback_number(self, callbacknumber):
+ """
+ Set the number that GoogleVoice calls
+ @param callbacknumber should be a proper 10 digit number
+ """
+ return self._gvoice.set_callback_number(callbacknumber)
+
+ def get_callback_number(self):
+ """
+ @returns Current callback number or None
+ """
+ return self._gvoice.get_callback_number()
+
+ def get_call_history(self, historyType):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ """
+ history = list(self._get_call_history(historyType))
+ history.sort(key=lambda item: item["time"])
+ return history
+
+ def _get_call_history(self, historyType):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ """
+ if historyType in [self.HISTORY_RECEIVED, self.HISTORY_ALL] or not self._received:
+ self._received = list(self._gvoice.get_received_calls())
+ for item in self._received:
+ item["action"] = self.HISTORY_RECEIVED
+ if historyType in [self.HISTORY_MISSED, self.HISTORY_ALL] or not self._missed:
+ self._missed = list(self._gvoice.get_missed_calls())
+ for item in self._missed:
+ item["action"] = self.HISTORY_MISSED
+ if historyType in [self.HISTORY_PLACED, self.HISTORY_ALL] or not self._placed:
+ self._placed = list(self._gvoice.get_placed_calls())
+ for item in self._placed:
+ item["action"] = self.HISTORY_PLACED
+ received = self._received
+ missed = self._missed
+ placed = self._placed
+ for item in received:
+ yield item
+ for item in missed:
+ yield item
+ for item in placed:
+ yield item
+
+ def get_messages(self, messageType):
+ messages = list(self._get_messages(messageType))
+ messages.sort(key=lambda message: message["time"])
+ return messages
+
+ def _get_messages(self, messageType):
+ if messageType in [self.MESSAGE_VOICEMAILS, self.MESSAGE_ALL] or not self._voicemails:
+ self._voicemails = list(self._gvoice.get_voicemails())
+ if messageType in [self.MESSAGE_TEXTS, self.MESSAGE_ALL] or not self._texts:
+ self._texts = list(self._gvoice.get_texts())
+ voicemails = self._voicemails
+ smss = self._texts
+
+ conversations = itertools.chain(voicemails, smss)
+ for conversation in conversations:
+ messages = conversation.messages
+ messageParts = [
+ (message.whoFrom, self._format_message(message), message.when)
+ for message in messages
+ ]
+
+ messageDetails = {
+ "id": conversation.id,
+ "contactId": conversation.contactId,
+ "name": conversation.name,
+ "time": conversation.time,
+ "relTime": conversation.relTime,
+ "prettyNumber": conversation.prettyNumber,
+ "number": conversation.number,
+ "location": conversation.location,
+ "messageParts": messageParts,
+ "type": conversation.type,
+ "isRead": conversation.isRead,
+ "isTrash": conversation.isTrash,
+ "isSpam": conversation.isSpam,
+ "isArchived": conversation.isArchived,
+ }
+ yield messageDetails
+
+ def clear_caches(self):
+ pass
+
+ def get_addressbooks(self):
+ """
+ @returns Iterable of (Address Book Factory, Book Id, Book Name)
+ """
+ yield self, "", ""
+
+ def open_addressbook(self, bookId):
+ return self
+
+ @staticmethod
+ def contact_source_short_name(contactId):
+ return "GV"
+
+ @staticmethod
+ def factory_name():
+ return "Google Voice"
+
+ def _format_message(self, message):
+ messagePartFormat = {
+ "med1": "<i>%s</i>",
+ "med2": "%s",
+ "high": "<b>%s</b>",
+ }
+ return " ".join(
+ messagePartFormat[text.accuracy] % io_utils.escape(text.text)
+ for text in message.body
+ )
+
+
+def sort_messages(allMessages):
+ sortableAllMessages = [
+ (message["time"], message)
+ for message in allMessages
+ ]
+ sortableAllMessages.sort(reverse=True)
+ return (
+ message
+ for (exactTime, message) in sortableAllMessages
+ )
+
+
+def decorate_recent(recentCallData):
+ """
+ @returns (personsName, phoneNumber, date, action)
+ """
+ contactId = recentCallData["contactId"]
+ if recentCallData["name"]:
+ header = recentCallData["name"]
+ elif recentCallData["prettyNumber"]:
+ header = recentCallData["prettyNumber"]
+ elif recentCallData["location"]:
+ header = recentCallData["location"]
+ else:
+ header = "Unknown"
+
+ number = recentCallData["number"]
+ relTime = recentCallData["relTime"]
+ action = recentCallData["action"]
+ return contactId, header, number, relTime, action
+
+
+def decorate_message(messageData):
+ contactId = messageData["contactId"]
+ exactTime = messageData["time"]
+ if messageData["name"]:
+ header = messageData["name"]
+ elif messageData["prettyNumber"]:
+ header = messageData["prettyNumber"]
+ else:
+ header = "Unknown"
+ number = messageData["number"]
+ relativeTime = messageData["relTime"]
+
+ messageParts = list(messageData["messageParts"])
+ if len(messageParts) == 0:
+ messages = ("No Transcription", )
+ elif len(messageParts) == 1:
+ messages = (messageParts[0][1], )
+ else:
+ messages = [
+ "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
+ for messagePart in messageParts
+ ]
+
+ decoratedResults = contactId, header, number, relativeTime, messages
+ return decoratedResults
--- /dev/null
+"""
+@author: Laszlo Nagy
+@copyright: (c) 2005 by Szoftver Messias Bt.
+@licence: BSD style
+
+Objects of the MozillaEmulator class can emulate a browser that is capable of:
+
+ - cookie management
+ - configurable user agent string
+ - GET and POST
+ - multipart POST (send files)
+ - receive content into file
+
+I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it:
+
+ 1. Use firefox
+ 2. Install and open the livehttpheaders plugin
+ 3. Use the website manually with firefox
+ 4. Check the GET and POST requests in the livehttpheaders capture window
+ 5. Create an instance of the above class and send the same GET and POST requests to the server.
+
+Optional steps:
+
+ - You can change user agent string in the build_opened method
+ - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files
+"""
+
+import urllib2
+import cookielib
+import logging
+
+import socket
+
+
+_moduleLogger = logging.getLogger(__name__)
+socket.setdefaulttimeout(25)
+
+
+def add_proxy(protocol, url, port):
+ proxyInfo = "%s:%s" % (url, port)
+ proxy = urllib2.ProxyHandler(
+ {protocol: proxyInfo}
+ )
+ opener = urllib2.build_opener(proxy)
+ urllib2.install_opener(opener)
+
+
+class MozillaEmulator(object):
+
+ USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)'
+ #USER_AGENT = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"
+
+ def __init__(self, trycount = 1):
+ """Create a new MozillaEmulator object.
+
+ @param trycount: The download() method will retry the operation if it
+ fails. You can specify -1 for infinite retrying. A value of 0 means no
+ retrying. A value of 1 means one retry. etc."""
+ self.debug = False
+ self.trycount = trycount
+ self._cookies = cookielib.LWPCookieJar()
+ self._loadedFromCookies = False
+ self._storeCookies = False
+
+ def load_cookies(self, path):
+ assert not self._loadedFromCookies, "Load cookies only once"
+ if path is None:
+ return
+
+ self._cookies.filename = path
+ try:
+ self._cookies.load()
+ except cookielib.LoadError:
+ _moduleLogger.exception("Bad cookie file")
+ except IOError:
+ _moduleLogger.exception("No cookie file")
+ except Exception, e:
+ _moduleLogger.exception("Unknown error with cookies")
+ else:
+ self._loadedFromCookies = True
+ self._storeCookies = True
+
+ return self._loadedFromCookies
+
+ def save_cookies(self):
+ if self._storeCookies:
+ self._cookies.save()
+
+ def clear_cookies(self):
+ if self._storeCookies:
+ self._cookies.clear()
+
+ def download(self, url,
+ postdata = None, extraheaders = None, forbidRedirect = False,
+ trycount = None, only_head = False,
+ ):
+ """Download an URL with GET or POST methods.
+
+ @param postdata: It can be a string that will be POST-ed to the URL.
+ When None is given, the method will be GET instead.
+ @param extraheaders: You can add/modify HTTP headers with a dict here.
+ @param forbidRedirect: Set this flag if you do not want to handle
+ HTTP 301 and 302 redirects.
+ @param trycount: Specify the maximum number of retries here.
+ 0 means no retry on error. Using -1 means infinite retring.
+ None means the default value (that is self.trycount).
+ @param only_head: Create the openerdirector and return it. In other
+ words, this will not retrieve any content except HTTP headers.
+
+ @return: The raw HTML page data
+ """
+ _moduleLogger.debug("Performing download of %s" % url)
+
+ if extraheaders is None:
+ extraheaders = {}
+ if trycount is None:
+ trycount = self.trycount
+ cnt = 0
+
+ while True:
+ try:
+ req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect)
+ openerdirector = u.open(req)
+ if self.debug:
+ _moduleLogger.info("%r - %r" % (req.get_method(), url))
+ _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg))
+ _moduleLogger.info("%r" % (openerdirector.headers))
+ self._cookies.extract_cookies(openerdirector, req)
+ if only_head:
+ return openerdirector
+
+ return self._read(openerdirector, trycount)
+ except urllib2.URLError, e:
+ _moduleLogger.debug("%s: %s" % (e, url))
+ cnt += 1
+ if (-1 < trycount) and (trycount < cnt):
+ raise
+
+ # Retry :-)
+ _moduleLogger.debug("MozillaEmulator: urllib2.URLError, retrying %d" % cnt)
+
+ def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False):
+ if extraheaders is None:
+ extraheaders = {}
+
+ txheaders = {
+ 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png',
+ 'Accept-Language': 'en,en-us;q=0.5',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'User-Agent': self.USER_AGENT,
+ }
+ for key, value in extraheaders.iteritems():
+ txheaders[key] = value
+ req = urllib2.Request(url, postdata, txheaders)
+ self._cookies.add_cookie_header(req)
+ if forbidRedirect:
+ redirector = HTTPNoRedirector()
+ #_moduleLogger.info("Redirection disabled")
+ else:
+ redirector = urllib2.HTTPRedirectHandler()
+ #_moduleLogger.info("Redirection enabled")
+
+ http_handler = urllib2.HTTPHandler(debuglevel=self.debug)
+ https_handler = urllib2.HTTPSHandler(debuglevel=self.debug)
+
+ u = urllib2.build_opener(
+ http_handler,
+ https_handler,
+ urllib2.HTTPCookieProcessor(self._cookies),
+ redirector
+ )
+ if not postdata is None:
+ req.add_data(postdata)
+ return (req, u)
+
+ def _read(self, openerdirector, trycount):
+ chunks = []
+
+ chunk = openerdirector.read()
+ chunks.append(chunk)
+ #while chunk and cnt < trycount:
+ # time.sleep(1)
+ # cnt += 1
+ # chunk = openerdirector.read()
+ # chunks.append(chunk)
+
+ data = "".join(chunks)
+
+ if "Content-Length" in openerdirector.info():
+ assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % (
+ openerdirector.info()["Content-Length"],
+ len(data),
+ )
+
+ return data
+
+
+class HTTPNoRedirector(urllib2.HTTPRedirectHandler):
+ """This is a custom http redirect handler that FORBIDS redirection."""
+
+ def http_error_302(self, req, fp, code, msg, headers):
+ e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
+ if e.code in (301, 302):
+ if 'location' in headers:
+ newurl = headers.getheaders('location')[0]
+ elif 'uri' in headers:
+ newurl = headers.getheaders('uri')[0]
+ e.newurl = newurl
+ _moduleLogger.info("New url: %s" % e.newurl)
+ raise e
--- /dev/null
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's GoogleVoice service.
+Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Google Voice backend code
+
+Resources
+ http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
+ http://posttopic.com/topic/google-voice-add-on-development
+"""
+
+from __future__ import with_statement
+
+import os
+import re
+import urllib
+import urllib2
+import time
+import datetime
+import itertools
+import logging
+import inspect
+
+from xml.sax import saxutils
+from xml.etree import ElementTree
+
+try:
+ import simplejson as _simplejson
+ simplejson = _simplejson
+except ImportError:
+ simplejson = None
+
+import browser_emu
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class NetworkError(RuntimeError):
+ pass
+
+
+class MessageText(object):
+
+ ACCURACY_LOW = "med1"
+ ACCURACY_MEDIUM = "med2"
+ ACCURACY_HIGH = "high"
+
+ def __init__(self):
+ self.accuracy = None
+ self.text = None
+
+ def __str__(self):
+ return self.text
+
+ def to_dict(self):
+ return to_dict(self)
+
+ def __eq__(self, other):
+ return self.accuracy == other.accuracy and self.text == other.text
+
+
+class Message(object):
+
+ def __init__(self):
+ self.whoFrom = None
+ self.body = None
+ self.when = None
+
+ def __str__(self):
+ return "%s (%s): %s" % (
+ self.whoFrom,
+ self.when,
+ "".join(unicode(part) for part in self.body)
+ )
+
+ def to_dict(self):
+ selfDict = to_dict(self)
+ selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
+ return selfDict
+
+ def __eq__(self, other):
+ return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
+
+
+class Conversation(object):
+
+ TYPE_VOICEMAIL = "Voicemail"
+ TYPE_SMS = "SMS"
+
+ def __init__(self):
+ self.type = None
+ self.id = None
+ self.contactId = None
+ self.name = None
+ self.location = None
+ self.prettyNumber = None
+ self.number = None
+
+ self.time = None
+ self.relTime = None
+ self.messages = None
+ self.isRead = None
+ self.isSpam = None
+ self.isTrash = None
+ self.isArchived = None
+
+ def __cmp__(self, other):
+ cmpValue = cmp(self.contactId, other.contactId)
+ if cmpValue != 0:
+ return cmpValue
+
+ cmpValue = cmp(self.time, other.time)
+ if cmpValue != 0:
+ return cmpValue
+
+ cmpValue = cmp(self.id, other.id)
+ if cmpValue != 0:
+ return cmpValue
+
+ def to_dict(self):
+ selfDict = to_dict(self)
+ selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
+ return selfDict
+
+
+class GVoiceBackend(object):
+ """
+ This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
+ the functions include login, setting up a callback number, and initalting a callback
+ """
+
+ PHONE_TYPE_HOME = 1
+ PHONE_TYPE_MOBILE = 2
+ PHONE_TYPE_WORK = 3
+ PHONE_TYPE_GIZMO = 7
+
+ def __init__(self, cookieFile = None):
+ # Important items in this function are the setup of the browser emulation and cookie file
+ self._browser = browser_emu.MozillaEmulator(1)
+ self._loadedFromCookies = self._browser.load_cookies(cookieFile)
+
+ self._token = ""
+ self._accountNum = ""
+ self._lastAuthed = 0.0
+ self._callbackNumber = ""
+ self._callbackNumbers = {}
+
+ # Suprisingly, moving all of these from class to self sped up startup time
+
+ self._validateRe = re.compile("^\+?[0-9]{10,}$")
+
+ self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+
+ SECURE_URL_BASE = "https://www.google.com/voice/"
+ SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
+ self._tokenURL = SECURE_URL_BASE + "m"
+ self._callUrl = SECURE_URL_BASE + "call/connect"
+ self._callCancelURL = SECURE_URL_BASE + "call/cancel"
+ self._sendSmsURL = SECURE_URL_BASE + "sms/send"
+
+ self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
+ self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
+ self._setDndURL = "https://www.google.com/voice/m/savednd"
+
+ self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
+ self._markAsReadURL = SECURE_URL_BASE + "m/mark"
+ self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
+
+ self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
+ self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
+ # HACK really this redirects to the main pge and we are grabbing some javascript
+ self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
+ self._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export"
+ self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user"
+ self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
+
+ self.XML_FEEDS = (
+ 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
+ 'recorded', 'placed', 'received', 'missed'
+ )
+ self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
+ self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
+ self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
+ self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
+ self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
+ self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
+ self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
+ self._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/"
+ self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread"
+ self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
+ self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
+ self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
+ self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
+
+ self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
+
+ self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
+ self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
+ self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
+ self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
+ self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
+ self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
+ self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
+ self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
+ self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
+ self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+
+ def is_quick_login_possible(self):
+ """
+ @returns True then refresh_account_info might be enough to login, else full login is required
+ """
+ return self._loadedFromCookies or 0.0 < self._lastAuthed
+
+ def refresh_account_info(self):
+ try:
+ page = self._get_page(self._JSON_CONTACTS_URL)
+ accountData = self._grab_account_info(page)
+ except Exception, e:
+ _moduleLogger.exception(str(e))
+ return None
+
+ self._browser.save_cookies()
+ self._lastAuthed = time.time()
+ return accountData
+
+ def _get_token(self):
+ tokenPage = self._get_page(self._tokenURL)
+
+ galxTokens = self._galxRe.search(tokenPage)
+ if galxTokens is not None:
+ galxToken = galxTokens.group(1)
+ else:
+ galxToken = ""
+ _moduleLogger.debug("Could not grab GALX token")
+ return galxToken
+
+ def _login(self, username, password, token):
+ loginData = {
+ 'Email' : username,
+ 'Passwd' : password,
+ 'service': "grandcentral",
+ "ltmpl": "mobile",
+ "btmpl": "mobile",
+ "PersistentCookie": "yes",
+ "GALX": token,
+ "continue": self._JSON_CONTACTS_URL,
+ }
+
+ loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
+ return loginSuccessOrFailurePage
+
+ def login(self, username, password):
+ """
+ Attempt to login to GoogleVoice
+ @returns Whether login was successful or not
+ @blocks
+ """
+ self.logout()
+ galxToken = self._get_token()
+ loginSuccessOrFailurePage = self._login(username, password, galxToken)
+
+ try:
+ accountData = self._grab_account_info(loginSuccessOrFailurePage)
+ except Exception, e:
+ # Retry in case the redirect failed
+ # luckily refresh_account_info does everything we need for a retry
+ accountData = self.refresh_account_info()
+ if accountData is None:
+ _moduleLogger.exception(str(e))
+ return None
+ _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
+
+ self._browser.save_cookies()
+ self._lastAuthed = time.time()
+ return accountData
+
+ def persist(self):
+ self._browser.save_cookies()
+
+ def shutdown(self):
+ self._browser.save_cookies()
+ self._token = None
+ self._lastAuthed = 0.0
+
+ def logout(self):
+ self._browser.clear_cookies()
+ self._browser.save_cookies()
+ self._token = None
+ self._lastAuthed = 0.0
+ self._callbackNumbers = {}
+
+ def is_dnd(self):
+ """
+ @blocks
+ """
+ isDndPage = self._get_page(self._isDndURL)
+
+ dndGroup = self._isDndRe.search(isDndPage)
+ if dndGroup is None:
+ return False
+ dndStatus = dndGroup.group(1)
+ isDnd = True if dndStatus.strip().lower() == "true" else False
+ return isDnd
+
+ def set_dnd(self, doNotDisturb):
+ """
+ @blocks
+ """
+ dndPostData = {
+ "doNotDisturb": 1 if doNotDisturb else 0,
+ }
+
+ dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
+
+ def call(self, outgoingNumber):
+ """
+ This is the main function responsible for initating the callback
+ @blocks
+ """
+ outgoingNumber = self._send_validation(outgoingNumber)
+ subscriberNumber = None
+ phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
+
+ callData = {
+ 'outgoingNumber': outgoingNumber,
+ 'forwardingNumber': self._callbackNumber,
+ 'subscriberNumber': subscriberNumber or 'undefined',
+ 'phoneType': str(phoneType),
+ 'remember': '1',
+ }
+ _moduleLogger.info("%r" % callData)
+
+ page = self._get_page_with_token(
+ self._callUrl,
+ callData,
+ )
+ self._parse_with_validation(page)
+ return True
+
+ def cancel(self, outgoingNumber=None):
+ """
+ Cancels a call matching outgoing and forwarding numbers (if given).
+ Will raise an error if no matching call is being placed
+ @blocks
+ """
+ page = self._get_page_with_token(
+ self._callCancelURL,
+ {
+ 'outgoingNumber': outgoingNumber or 'undefined',
+ 'forwardingNumber': self._callbackNumber or 'undefined',
+ 'cancelType': 'C2C',
+ },
+ )
+ self._parse_with_validation(page)
+
+ def send_sms(self, phoneNumbers, message):
+ """
+ @blocks
+ """
+ validatedPhoneNumbers = [
+ self._send_validation(phoneNumber)
+ for phoneNumber in phoneNumbers
+ ]
+ flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
+ page = self._get_page_with_token(
+ self._sendSmsURL,
+ {
+ 'phoneNumber': flattenedPhoneNumbers,
+ 'text': unicode(message).encode("utf-8"),
+ },
+ )
+ self._parse_with_validation(page)
+
+ def search(self, query):
+ """
+ Search your Google Voice Account history for calls, voicemails, and sms
+ Returns ``Folder`` instance containting matching messages
+ @blocks
+ """
+ page = self._get_page(
+ self._XML_SEARCH_URL,
+ {"q": query},
+ )
+ json, html = extract_payload(page)
+ return json
+
+ def get_feed(self, feed):
+ """
+ @blocks
+ """
+ actualFeed = "_XML_%s_URL" % feed.upper()
+ feedUrl = getattr(self, actualFeed)
+
+ page = self._get_page(feedUrl)
+ json, html = extract_payload(page)
+
+ return json
+
+ def recording_url(self, messageId):
+ url = self._downloadVoicemailURL+messageId
+ return url
+
+ def download(self, messageId, targetPath):
+ """
+ Download a voicemail or recorded call MP3 matching the given ``msg``
+ which can either be a ``Message`` instance, or a SHA1 identifier.
+ Message hashes can be found in ``self.voicemail().messages`` for example.
+ @returns location of saved file.
+ @blocks
+ """
+ page = self._get_page(self.recording_url(messageId))
+ with open(targetPath, 'wb') as fo:
+ fo.write(page)
+
+ def is_valid_syntax(self, number):
+ """
+ @returns If This number be called ( syntax validation only )
+ """
+ return self._validateRe.match(number) is not None
+
+ def get_account_number(self):
+ """
+ @returns The GoogleVoice phone number
+ """
+ return self._accountNum
+
+ def get_callback_numbers(self):
+ """
+ @returns a dictionary mapping call back numbers to descriptions
+ @note These results are cached for 30 minutes.
+ """
+ return self._callbackNumbers
+
+ def set_callback_number(self, callbacknumber):
+ """
+ Set the number that GoogleVoice calls
+ @param callbacknumber should be a proper 10 digit number
+ """
+ self._callbackNumber = callbacknumber
+ _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
+ return True
+
+ def get_callback_number(self):
+ """
+ @returns Current callback number or None
+ """
+ return self._callbackNumber
+
+ def get_received_calls(self):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ @blocks
+ """
+ return self._parse_recent(self._get_page(self._XML_RECEIVED_URL))
+
+ def get_missed_calls(self):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ @blocks
+ """
+ return self._parse_recent(self._get_page(self._XML_MISSED_URL))
+
+ def get_placed_calls(self):
+ """
+ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+ @blocks
+ """
+ return self._parse_recent(self._get_page(self._XML_PLACED_URL))
+
+ def get_csv_contacts(self):
+ data = {
+ "groupToExport": "mine",
+ "exportType": "ALL",
+ "out": "OUTLOOK_CSV",
+ }
+ encodedData = urllib.urlencode(data)
+ contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
+ return contacts
+
+ def get_voicemails(self):
+ """
+ @blocks
+ """
+ voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
+ voicemailHtml = self._grab_html(voicemailPage)
+ voicemailJson = self._grab_json(voicemailPage)
+ if voicemailJson is None:
+ return ()
+ parsedVoicemail = self._parse_voicemail(voicemailHtml)
+ voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
+ return voicemails
+
+ def get_texts(self):
+ """
+ @blocks
+ """
+ smsPage = self._get_page(self._XML_SMS_URL)
+ smsHtml = self._grab_html(smsPage)
+ smsJson = self._grab_json(smsPage)
+ if smsJson is None:
+ return ()
+ parsedSms = self._parse_sms(smsHtml)
+ smss = self._merge_conversation_sources(parsedSms, smsJson)
+ return smss
+
+ def get_unread_counts(self):
+ countPage = self._get_page(self._JSON_SMS_COUNT_URL)
+ counts = parse_json(countPage)
+ counts = counts["unreadCounts"]
+ return counts
+
+ def mark_message(self, messageId, asRead):
+ """
+ @blocks
+ """
+ postData = {
+ "read": 1 if asRead else 0,
+ "id": messageId,
+ }
+
+ markPage = self._get_page(self._markAsReadURL, postData)
+
+ def archive_message(self, messageId):
+ """
+ @blocks
+ """
+ postData = {
+ "id": messageId,
+ }
+
+ markPage = self._get_page(self._archiveMessageURL, postData)
+
+ def _grab_json(self, flatXml):
+ xmlTree = ElementTree.fromstring(flatXml)
+ jsonElement = xmlTree.getchildren()[0]
+ flatJson = jsonElement.text
+ jsonTree = parse_json(flatJson)
+ return jsonTree
+
+ def _grab_html(self, flatXml):
+ xmlTree = ElementTree.fromstring(flatXml)
+ htmlElement = xmlTree.getchildren()[1]
+ flatHtml = htmlElement.text
+ return flatHtml
+
+ def _grab_account_info(self, page):
+ accountData = parse_json(page)
+ self._token = accountData["r"]
+ self._accountNum = accountData["number"]["raw"]
+ for callback in accountData["phones"].itervalues():
+ self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
+ if len(self._callbackNumbers) == 0:
+ _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
+ return accountData
+
+ def _send_validation(self, number):
+ if not self.is_valid_syntax(number):
+ raise ValueError('Number is not valid: "%s"' % number)
+ return number
+
+ def _parse_recent(self, recentPage):
+ allRecentHtml = self._grab_html(recentPage)
+ allRecentData = self._parse_history(allRecentHtml)
+ for recentCallData in allRecentData:
+ yield recentCallData
+
+ def _parse_history(self, historyHtml):
+ splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
+ for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
+ exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+ exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ exactTime = google_strptime(exactTime)
+ relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+ relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ locationGroup = self._voicemailLocationRegex.search(messageHtml)
+ location = locationGroup.group(1).strip() if locationGroup else ""
+
+ nameGroup = self._voicemailNameRegex.search(messageHtml)
+ name = nameGroup.group(1).strip() if nameGroup else ""
+ numberGroup = self._voicemailNumberRegex.search(messageHtml)
+ number = numberGroup.group(1).strip() if numberGroup else ""
+ prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+ prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+
+ yield {
+ "id": messageId.strip(),
+ "contactId": contactId,
+ "name": unescape(name),
+ "time": exactTime,
+ "relTime": relativeTime,
+ "prettyNumber": prettyNumber,
+ "number": number,
+ "location": unescape(location),
+ }
+
+ @staticmethod
+ def _interpret_voicemail_regex(group):
+ quality, content, number = group.group(2), group.group(3), group.group(4)
+ text = MessageText()
+ if quality is not None and content is not None:
+ text.accuracy = quality
+ text.text = unescape(content)
+ return text
+ elif number is not None:
+ text.accuracy = MessageText.ACCURACY_HIGH
+ text.text = number
+ return text
+
+ def _parse_voicemail(self, voicemailHtml):
+ splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
+ for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
+ conv = Conversation()
+ conv.type = Conversation.TYPE_VOICEMAIL
+ conv.id = messageId.strip()
+
+ exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+ exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ conv.time = google_strptime(exactTimeText)
+ relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+ conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ locationGroup = self._voicemailLocationRegex.search(messageHtml)
+ conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
+
+ nameGroup = self._voicemailNameRegex.search(messageHtml)
+ conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
+ numberGroup = self._voicemailNumberRegex.search(messageHtml)
+ conv.number = numberGroup.group(1).strip() if numberGroup else ""
+ prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+ conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+
+ messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
+ messageParts = [
+ self._interpret_voicemail_regex(group)
+ for group in messageGroups
+ ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
+ message = Message()
+ message.body = messageParts
+ message.whoFrom = conv.name
+ try:
+ message.when = conv.time.strftime("%I:%M %p")
+ except ValueError:
+ _moduleLogger.exception("Confusing time provided: %r" % conv.time)
+ message.when = "Unknown"
+ conv.messages = (message, )
+
+ yield conv
+
+ @staticmethod
+ def _interpret_sms_message_parts(fromPart, textPart, timePart):
+ text = MessageText()
+ text.accuracy = MessageText.ACCURACY_MEDIUM
+ text.text = unescape(textPart)
+
+ message = Message()
+ message.body = (text, )
+ message.whoFrom = fromPart
+ message.when = timePart
+
+ return message
+
+ def _parse_sms(self, smsHtml):
+ splitSms = self._seperateVoicemailsRegex.split(smsHtml)
+ for messageId, messageHtml in itergroup(splitSms[1:], 2):
+ conv = Conversation()
+ conv.type = Conversation.TYPE_SMS
+ conv.id = messageId.strip()
+
+ exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+ exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ conv.time = google_strptime(exactTimeText)
+ relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+ conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ conv.location = ""
+
+ nameGroup = self._voicemailNameRegex.search(messageHtml)
+ conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
+ numberGroup = self._voicemailNumberRegex.search(messageHtml)
+ conv.number = numberGroup.group(1).strip() if numberGroup else ""
+ prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+ conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+
+ fromGroups = self._smsFromRegex.finditer(messageHtml)
+ fromParts = (group.group(1).strip() for group in fromGroups)
+ textGroups = self._smsTextRegex.finditer(messageHtml)
+ textParts = (group.group(1).strip() for group in textGroups)
+ timeGroups = self._smsTimeRegex.finditer(messageHtml)
+ timeParts = (group.group(1).strip() for group in timeGroups)
+
+ messageParts = itertools.izip(fromParts, textParts, timeParts)
+ messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
+ conv.messages = messages
+
+ yield conv
+
+ @staticmethod
+ def _merge_conversation_sources(parsedMessages, json):
+ for message in parsedMessages:
+ jsonItem = json["messages"][message.id]
+ message.isRead = jsonItem["isRead"]
+ message.isSpam = jsonItem["isSpam"]
+ message.isTrash = jsonItem["isTrash"]
+ message.isArchived = "inbox" not in jsonItem["labels"]
+ yield message
+
+ def _get_page(self, url, data = None, refererUrl = None):
+ headers = {}
+ if refererUrl is not None:
+ headers["Referer"] = refererUrl
+
+ encodedData = urllib.urlencode(data) if data is not None else None
+
+ try:
+ page = self._browser.download(url, encodedData, None, headers)
+ except urllib2.URLError, e:
+ _moduleLogger.error("Translating error: %s" % str(e))
+ raise NetworkError("%s is not accesible" % url)
+
+ return page
+
+ def _get_page_with_token(self, url, data = None, refererUrl = None):
+ if data is None:
+ data = {}
+ data['_rnr_se'] = self._token
+
+ page = self._get_page(url, data, refererUrl)
+
+ return page
+
+ def _parse_with_validation(self, page):
+ json = parse_json(page)
+ self._validate_response(json)
+ return json
+
+ def _validate_response(self, response):
+ """
+ Validates that the JSON response is A-OK
+ """
+ try:
+ assert response is not None, "Response not provided"
+ assert 'ok' in response, "Response lacks status"
+ assert response['ok'], "Response not good"
+ except AssertionError:
+ try:
+ if response["data"]["code"] == 20:
+ raise RuntimeError(
+"""Ambiguous error 20 returned by Google Voice.
+Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber)
+ except KeyError:
+ pass
+ raise RuntimeError('There was a problem with GV: %s' % response)
+
+
+_UNESCAPE_ENTITIES = {
+ """: '"',
+ " ": " ",
+ "'": "'",
+}
+
+
+def unescape(text):
+ plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
+ return plain
+
+
+def google_strptime(time):
+ """
+ Hack: Google always returns the time in the same locale. Sadly if the
+ local system's locale is different, there isn't a way to perfectly handle
+ the time. So instead we handle implement some time formatting
+ """
+ abbrevTime = time[:-3]
+ parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
+ if time.endswith("PM"):
+ parsedTime += datetime.timedelta(hours=12)
+ return parsedTime
+
+
+def itergroup(iterator, count, padValue = None):
+ """
+ Iterate in groups of 'count' values. If there
+ aren't enough values, the last result is padded with
+ None.
+
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+ ... print tuple(val)
+ (1, 2, 3)
+ (4, 5, 6)
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
+ ... print list(val)
+ [1, 2, 3]
+ [4, 5, 6]
+ >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
+ ... print tuple(val)
+ (1, 2, 3)
+ (4, 5, 6)
+ (7, None, None)
+ >>> for val in itergroup("123456", 3):
+ ... print tuple(val)
+ ('1', '2', '3')
+ ('4', '5', '6')
+ >>> for val in itergroup("123456", 3):
+ ... print repr("".join(val))
+ '123'
+ '456'
+ """
+ paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
+ nIterators = (paddedIterator, ) * count
+ return itertools.izip(*nIterators)
+
+
+def safe_eval(s):
+ _TRUE_REGEX = re.compile("true")
+ _FALSE_REGEX = re.compile("false")
+ _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
+ s = _TRUE_REGEX.sub("True", s)
+ s = _FALSE_REGEX.sub("False", s)
+ s = _COMMENT_REGEX.sub("#", s)
+ try:
+ results = eval(s, {}, {})
+ except SyntaxError:
+ _moduleLogger.exception("Oops")
+ results = None
+ return results
+
+
+def _fake_parse_json(flattened):
+ return safe_eval(flattened)
+
+
+def _actual_parse_json(flattened):
+ return simplejson.loads(flattened)
+
+
+if simplejson is None:
+ parse_json = _fake_parse_json
+else:
+ parse_json = _actual_parse_json
+
+
+def extract_payload(flatXml):
+ xmlTree = ElementTree.fromstring(flatXml)
+
+ jsonElement = xmlTree.getchildren()[0]
+ flatJson = jsonElement.text
+ jsonTree = parse_json(flatJson)
+
+ htmlElement = xmlTree.getchildren()[1]
+ flatHtml = htmlElement.text
+
+ return jsonTree, flatHtml
+
+
+def guess_phone_type(number):
+ if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
+ return GVoiceBackend.PHONE_TYPE_GIZMO
+ else:
+ return GVoiceBackend.PHONE_TYPE_MOBILE
+
+
+def get_sane_callback(backend):
+ """
+ Try to set a sane default callback number on these preferences
+ 1) 1747 numbers ( Gizmo )
+ 2) anything with gizmo in the name
+ 3) anything with computer in the name
+ 4) the first value
+ """
+ numbers = backend.get_callback_numbers()
+
+ priorityOrderedCriteria = [
+ ("\+1747", None),
+ ("1747", None),
+ ("747", None),
+ (None, "gizmo"),
+ (None, "computer"),
+ (None, "sip"),
+ (None, None),
+ ]
+
+ for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
+ numberMatcher = None
+ descriptionMatcher = None
+ if numberCriteria is not None:
+ numberMatcher = re.compile(numberCriteria)
+ elif descriptionCriteria is not None:
+ descriptionMatcher = re.compile(descriptionCriteria, re.I)
+
+ for number, description in numbers.iteritems():
+ if numberMatcher is not None and numberMatcher.match(number) is None:
+ continue
+ if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
+ continue
+ return number
+
+
+def set_sane_callback(backend):
+ """
+ Try to set a sane default callback number on these preferences
+ 1) 1747 numbers ( Gizmo )
+ 2) anything with gizmo in the name
+ 3) anything with computer in the name
+ 4) the first value
+ """
+ number = get_sane_callback(backend)
+ backend.set_callback_number(number)
+
+
+def _is_not_special(name):
+ return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
+
+
+def to_dict(obj):
+ members = inspect.getmembers(obj)
+ return dict((name, value) for (name, value) in members if _is_not_special(name))
+
+
+def grab_debug_info(username, password):
+ cookieFile = os.path.join(".", "raw_cookies.txt")
+ try:
+ os.remove(cookieFile)
+ except OSError:
+ pass
+
+ backend = GVoiceBackend(cookieFile)
+ browser = backend._browser
+
+ _TEST_WEBPAGES = [
+ ("token", backend._tokenURL),
+ ("login", backend._loginURL),
+ ("isdnd", backend._isDndURL),
+ ("account", backend._XML_ACCOUNT_URL),
+ ("html_contacts", backend._XML_CONTACTS_URL),
+ ("contacts", backend._JSON_CONTACTS_URL),
+ ("csv", backend._CSV_CONTACTS_URL),
+
+ ("voicemail", backend._XML_VOICEMAIL_URL),
+ ("html_sms", backend._XML_SMS_URL),
+ ("sms", backend._JSON_SMS_URL),
+ ("count", backend._JSON_SMS_COUNT_URL),
+
+ ("recent", backend._XML_RECENT_URL),
+ ("placed", backend._XML_PLACED_URL),
+ ("recieved", backend._XML_RECEIVED_URL),
+ ("missed", backend._XML_MISSED_URL),
+ ]
+
+ # Get Pages
+ print "Grabbing pre-login pages"
+ for name, url in _TEST_WEBPAGES:
+ try:
+ page = browser.download(url)
+ except StandardError, e:
+ print e.message
+ continue
+ print "\tWriting to file"
+ with open("not_loggedin_%s.txt" % name, "w") as f:
+ f.write(page)
+
+ # Login
+ print "Attempting login"
+ galxToken = backend._get_token()
+ loginSuccessOrFailurePage = backend._login(username, password, galxToken)
+ with open("loggingin.txt", "w") as f:
+ print "\tWriting to file"
+ f.write(loginSuccessOrFailurePage)
+ try:
+ backend._grab_account_info(loginSuccessOrFailurePage)
+ except Exception:
+ # Retry in case the redirect failed
+ # luckily refresh_account_info does everything we need for a retry
+ loggedIn = backend.refresh_account_info() is not None
+ if not loggedIn:
+ raise
+
+ # Get Pages
+ print "Grabbing post-login pages"
+ for name, url in _TEST_WEBPAGES:
+ try:
+ page = browser.download(url)
+ except StandardError, e:
+ print str(e)
+ continue
+ print "\tWriting to file"
+ with open("loggedin_%s.txt" % name, "w") as f:
+ f.write(page)
+
+ # Cookies
+ browser.save_cookies()
+ print "\tWriting cookies to file"
+ with open("cookies.txt", "w") as f:
+ f.writelines(
+ "%s: %s\n" % (c.name, c.value)
+ for c in browser._cookies
+ )
+
+
+def grab_voicemails(username, password):
+ cookieFile = os.path.join(".", "raw_cookies.txt")
+ try:
+ os.remove(cookieFile)
+ except OSError:
+ pass
+
+ backend = GVoiceBackend(cookieFile)
+ backend.login(username, password)
+ voicemails = list(backend.get_voicemails())
+ for voicemail in voicemails:
+ print voicemail.id
+ backend.download(voicemail.id, ".")
+
+
+def main():
+ import sys
+ logging.basicConfig(level=logging.DEBUG)
+ args = sys.argv
+ if 3 <= len(args):
+ username = args[1]
+ password = args[2]
+
+ grab_debug_info(username, password)
+ grab_voicemails(username, password)
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+#!/usr/bin/python
+
+"""
+DialCentral - Front end for Google's Grand Central service.
+Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+
+
+class NullAddressBook(object):
+
+ @property
+ def name(self):
+ return "None"
+
+ def update_account(self, force = True):
+ pass
+
+ def get_contacts(self):
+ return {}
+
+
+class NullAddressBookFactory(object):
+
+ def get_addressbooks(self):
+ yield NullAddressBook()
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+if qt_compat.USES_PYSIDE:
+ try:
+ import QtMobility.Contacts as _QtContacts
+ QtContacts = _QtContacts
+ except ImportError:
+ QtContacts = None
+else:
+ QtContacts = None
+
+import null_backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class QtContactsAddressBook(object):
+
+ def __init__(self, name, uri):
+ self._name = name
+ self._uri = uri
+ self._manager = QtContacts.QContactManager.fromUri(uri)
+ self._contacts = None
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def error(self):
+ return self._manager.error()
+
+ def update_account(self, force = True):
+ if not force and self._contacts is not None:
+ return
+ self._contacts = dict(self._get_contacts())
+
+ def get_contacts(self):
+ if self._contacts is None:
+ self._contacts = dict(self._get_contacts())
+ return self._contacts
+
+ def _get_contacts(self):
+ contacts = self._manager.contacts()
+ for contact in contacts:
+ contactId = contact.localId()
+ contactName = contact.displayLabel()
+ phoneDetails = contact.details(QtContacts.QContactPhoneNumber().DefinitionName)
+ phones = [{"phoneType": "Phone", "phoneNumber": phone.value(QtContacts.QContactPhoneNumber().FieldNumber)} for phone in phoneDetails]
+ contactDetails = phones
+ if 0 < len(contactDetails):
+ yield str(contactId), {
+ "contactId": str(contactId),
+ "name": contactName,
+ "numbers": contactDetails,
+ }
+
+
+class _QtContactsAddressBookFactory(object):
+
+ def __init__(self):
+ self._availableManagers = {}
+
+ availableMgrs = QtContacts.QContactManager.availableManagers()
+ availableMgrs.remove("invalid")
+ for managerName in availableMgrs:
+ params = {}
+ managerUri = QtContacts.QContactManager.buildUri(managerName, params)
+ self._availableManagers[managerName] = managerUri
+
+ def get_addressbooks(self):
+ for name, uri in self._availableManagers.iteritems():
+ book = QtContactsAddressBook(name, uri)
+ if book.error:
+ _moduleLogger.info("Could not load %r due to %r" % (name, book.error))
+ else:
+ yield book
+
+
+class _EmptyAddressBookFactory(object):
+
+ def get_addressbooks(self):
+ if False:
+ yield None
+
+
+if QtContacts is not None:
+ QtContactsAddressBookFactory = _QtContactsAddressBookFactory
+else:
+ QtContactsAddressBookFactory = _EmptyAddressBookFactory
+ _moduleLogger.info("QtContacts support not available")
+
+
+if __name__ == "__main__":
+ factory = QtContactsAddressBookFactory()
+ books = factory.get_addressbooks()
+ for book in books:
+ print book.name
+ print book.get_contacts()
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+import dbus
+try:
+ import telepathy as _telepathy
+ import util.tp_utils as telepathy_utils
+ telepathy = _telepathy
+except ImportError:
+ telepathy = None
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class _FakeSignaller(object):
+
+ def start(self):
+ pass
+
+ def stop(self):
+ pass
+
+
+class _MissedCallWatcher(QtCore.QObject):
+
+ callMissed = qt_compat.Signal()
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._isStarted = False
+ self._isSupported = True
+
+ self._newChannelSignaller = telepathy_utils.NewChannelSignaller(self._on_new_channel)
+ self._outstandingRequests = []
+
+ @property
+ def isSupported(self):
+ return self._isSupported
+
+ @property
+ def isStarted(self):
+ return self._isStarted
+
+ def start(self):
+ if self._isStarted:
+ _moduleLogger.info("voicemail monitor already started")
+ return
+ try:
+ self._newChannelSignaller.start()
+ except RuntimeError:
+ _moduleLogger.exception("Missed call detection not supported")
+ self._newChannelSignaller = _FakeSignaller()
+ self._isSupported = False
+ self._isStarted = True
+
+ def stop(self):
+ if not self._isStarted:
+ _moduleLogger.info("voicemail monitor stopped without starting")
+ return
+ _moduleLogger.info("Stopping voicemail refresh")
+ self._newChannelSignaller.stop()
+
+ # I don't want to trust whether the cancel happens within the current
+ # callback or not which could be the deciding factor between invalid
+ # iterators or infinite loops
+ localRequests = [r for r in self._outstandingRequests]
+ for request in localRequests:
+ localRequests.cancel()
+
+ self._isStarted = False
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_new_channel(self, bus, serviceName, connObjectPath, channelObjectPath, channelType):
+ if channelType != telepathy.interfaces.CHANNEL_TYPE_STREAMED_MEDIA:
+ return
+
+ conn = telepathy.client.Connection(serviceName, connObjectPath)
+ try:
+ chan = telepathy.client.Channel(serviceName, channelObjectPath)
+ except dbus.exceptions.UnknownMethodException:
+ _moduleLogger.exception("Client might not have implemented a deprecated method")
+ return
+ missDetection = telepathy_utils.WasMissedCall(
+ bus, conn, chan, self._on_missed_call, self._on_error_for_missed
+ )
+ self._outstandingRequests.append(missDetection)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_missed_call(self, missDetection):
+ _moduleLogger.info("Missed a call")
+ self.callMissed.emit()
+ self._outstandingRequests.remove(missDetection)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_error_for_missed(self, missDetection, reason):
+ _moduleLogger.debug("Error: %r claims %r" % (missDetection, reason))
+ self._outstandingRequests.remove(missDetection)
+
+
+class _DummyMissedCallWatcher(QtCore.QObject):
+
+ callMissed = qt_compat.Signal()
+
+ def __init__(self):
+ QtCore.QObject.__init__(self)
+ self._isStarted = False
+
+ @property
+ def isSupported(self):
+ return False
+
+ @property
+ def isStarted(self):
+ return self._isStarted
+
+ def start(self):
+ self._isStarted = True
+
+ def stop(self):
+ if not self._isStarted:
+ _moduleLogger.info("voicemail monitor stopped without starting")
+ return
+ _moduleLogger.info("Stopping voicemail refresh")
+ self._isStarted = False
+
+
+if telepathy is not None:
+ MissedCallWatcher = _MissedCallWatcher
+else:
+ MissedCallWatcher = _DummyMissedCallWatcher
+
+
+if __name__ == "__main__":
+ pass
+
--- /dev/null
+import os
+
+__pretty_app_name__ = "DialCentral"
+__app_name__ = "dialcentral"
+__version__ = "1.3.6"
+__build__ = 0
+__app_magic__ = 0xdeadbeef
+_data_path_ = os.path.join(os.path.expanduser("~"), ".%s" % __app_name__)
+_user_settings_ = "%s/settings.ini" % _data_path_
+_custom_notifier_settings_ = "%s/notifier.ini" % _data_path_
+_user_logpath_ = "%s/%s.log" % (_data_path_, __app_name__)
+_notifier_logpath_ = "%s/notifier.log" % _data_path_
+IS_MAEMO = True
--- /dev/null
+#!/usr/bin/env python
+# -*- coding: UTF8 -*-
+
+from __future__ import with_statement
+
+import os
+import base64
+import ConfigParser
+import functools
+import logging
+import logging.handlers
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import constants
+import alarm_handler
+from util import qtpie
+from util import qwrappers
+from util import qui_utils
+from util import misc as misc_utils
+
+import session
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class Dialcentral(qwrappers.ApplicationWrapper):
+
+ _DATA_PATHS = [
+ os.path.join(os.path.dirname(__file__), "../share"),
+ os.path.join(os.path.dirname(__file__), "../data"),
+ ]
+
+ def __init__(self, app):
+ self._dataPath = None
+ self._aboutDialog = None
+ self.notifyOnMissed = False
+ self.notifyOnVoicemail = False
+ self.notifyOnSms = False
+
+ self._streamHandler = None
+ self._ledHandler = None
+ self._alarmHandler = alarm_handler.AlarmHandler()
+
+ qwrappers.ApplicationWrapper.__init__(self, app, constants)
+
+ def load_settings(self):
+ try:
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ except IOError, e:
+ _moduleLogger.info("No settings")
+ return
+ except ValueError:
+ _moduleLogger.info("Settings were corrupt")
+ return
+ except ConfigParser.MissingSectionHeaderError:
+ _moduleLogger.info("Settings were corrupt")
+ return
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+
+ self._mainWindow.load_settings(config)
+
+ def save_settings(self):
+ _moduleLogger.info("Saving settings")
+ config = ConfigParser.SafeConfigParser()
+
+ self._mainWindow.save_settings(config)
+
+ with open(constants._user_settings_, "wb") as configFile:
+ config.write(configFile)
+
+ def get_icon(self, name):
+ if self._dataPath is None:
+ for path in self._DATA_PATHS:
+ if os.path.exists(os.path.join(path, name)):
+ self._dataPath = path
+ break
+ if self._dataPath is not None:
+ icon = QtGui.QIcon(os.path.join(self._dataPath, name))
+ return icon
+ else:
+ return None
+
+ def get_resource(self, name):
+ if self._dataPath is None:
+ for path in self._DATA_PATHS:
+ if os.path.exists(os.path.join(path, name)):
+ self._dataPath = path
+ break
+ if self._dataPath is not None:
+ return os.path.join(self._dataPath, name)
+ else:
+ return None
+
+ def _close_windows(self):
+ qwrappers.ApplicationWrapper._close_windows(self)
+ if self._aboutDialog is not None:
+ self._aboutDialog.close()
+
+ @property
+ def fsContactsPath(self):
+ return os.path.join(constants._data_path_, "contacts")
+
+ @property
+ def streamHandler(self):
+ if self._streamHandler is None:
+ import stream_handler
+ self._streamHandler = stream_handler.StreamHandler()
+ return self._streamHandler
+
+ @property
+ def alarmHandler(self):
+ return self._alarmHandler
+
+ @property
+ def ledHandler(self):
+ if self._ledHandler is None:
+ import led_handler
+ self._ledHandler = led_handler.LedHandler()
+ return self._ledHandler
+
+ def _new_main_window(self):
+ return MainWindow(None, self)
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_about(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ if self._aboutDialog is None:
+ import dialogs
+ self._aboutDialog = dialogs.AboutDialog(self)
+ response = self._aboutDialog.run(self._mainWindow.window)
+
+
+class DelayedWidget(object):
+
+ def __init__(self, app, settingsNames):
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.setContentsMargins(0, 0, 0, 0)
+ self._widget = QtGui.QWidget()
+ self._widget.setContentsMargins(0, 0, 0, 0)
+ self._widget.setLayout(self._layout)
+ self._settings = dict((name, "") for name in settingsNames)
+
+ self._child = None
+ self._isEnabled = True
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def has_child(self):
+ return self._child is not None
+
+ def set_child(self, child):
+ if self._child is not None:
+ self._layout.removeWidget(self._child.toplevel)
+ self._child = child
+ if self._child is not None:
+ self._layout.addWidget(self._child.toplevel)
+
+ self._child.set_settings(self._settings)
+
+ if self._isEnabled:
+ self._child.enable()
+ else:
+ self._child.disable()
+
+ @property
+ def child(self):
+ return self._child
+
+ def enable(self):
+ self._isEnabled = True
+ if self._child is not None:
+ self._child.enable()
+
+ def disable(self):
+ self._isEnabled = False
+ if self._child is not None:
+ self._child.disable()
+
+ def clear(self):
+ if self._child is not None:
+ self._child.clear()
+
+ def refresh(self, force=True):
+ if self._child is not None:
+ self._child.refresh(force)
+
+ def get_settings(self):
+ if self._child is not None:
+ return self._child.get_settings()
+ else:
+ return self._settings
+
+ def set_settings(self, settings):
+ if self._child is not None:
+ self._child.set_settings(settings)
+ else:
+ self._settings = settings
+
+
+def _tab_factory(tab, app, session, errorLog):
+ import gv_views
+ return gv_views.__dict__[tab](app, session, errorLog)
+
+
+class MainWindow(qwrappers.WindowWrapper):
+
+ KEYPAD_TAB = 0
+ RECENT_TAB = 1
+ MESSAGES_TAB = 2
+ CONTACTS_TAB = 3
+ MAX_TABS = 4
+
+ _TAB_TITLES = [
+ "Dialpad",
+ "History",
+ "Messages",
+ "Contacts",
+ ]
+ assert len(_TAB_TITLES) == MAX_TABS
+
+ _TAB_ICONS = [
+ "dialpad.png",
+ "history.png",
+ "messages.png",
+ "contacts.png",
+ ]
+ assert len(_TAB_ICONS) == MAX_TABS
+
+ _TAB_CLASS = [
+ functools.partial(_tab_factory, "Dialpad"),
+ functools.partial(_tab_factory, "History"),
+ functools.partial(_tab_factory, "Messages"),
+ functools.partial(_tab_factory, "Contacts"),
+ ]
+ assert len(_TAB_CLASS) == MAX_TABS
+
+ # Hack to allow delay importing/loading of tabs
+ _TAB_SETTINGS_NAMES = [
+ (),
+ ("filter", ),
+ ("status", "type"),
+ ("selectedAddressbook", ),
+ ]
+ assert len(_TAB_SETTINGS_NAMES) == MAX_TABS
+
+ def __init__(self, parent, app):
+ qwrappers.WindowWrapper.__init__(self, parent, app)
+ self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
+ self._window.resized.connect(self._on_window_resized)
+ self._errorLog = self._app.errorLog
+
+ self._session = session.Session(self._errorLog, constants._data_path_)
+ self._session.error.connect(self._on_session_error)
+ self._session.loggedIn.connect(self._on_login)
+ self._session.loggedOut.connect(self._on_logout)
+ self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+ self._session.newMessages.connect(self._on_new_message_alert)
+ self._app.alarmHandler.applicationNotifySignal.connect(self._on_app_alert)
+ self._voicemailRefreshDelay = QtCore.QTimer()
+ self._voicemailRefreshDelay.setInterval(30 * 1000)
+ self._voicemailRefreshDelay.timeout.connect(self._on_call_missed)
+ self._voicemailRefreshDelay.setSingleShot(True)
+ self._callHandler = None
+ self._updateVoicemailOnMissedCall = False
+
+ self._defaultCredentials = "", ""
+ self._curentCredentials = "", ""
+ self._currentTab = 0
+
+ self._credentialsDialog = None
+ self._smsEntryDialog = None
+ self._accountDialog = None
+
+ self._tabsContents = [
+ DelayedWidget(self._app, self._TAB_SETTINGS_NAMES[i])
+ for i in xrange(self.MAX_TABS)
+ ]
+ for tab in self._tabsContents:
+ tab.disable()
+
+ self._tabWidget = QtGui.QTabWidget()
+ if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+ else:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+ defaultTabIconSize = self._tabWidget.iconSize()
+ defaultTabIconWidth, defaultTabIconHeight = defaultTabIconSize.width(), defaultTabIconSize.height()
+ for tabIndex, (tabTitle, tabIcon) in enumerate(
+ zip(self._TAB_TITLES, self._TAB_ICONS)
+ ):
+ icon = self._app.get_icon(tabIcon)
+ if constants.IS_MAEMO and icon is not None:
+ tabTitle = ""
+
+ if icon is None:
+ self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
+ else:
+ iconSize = icon.availableSizes()[0]
+ defaultTabIconWidth = max(defaultTabIconWidth, iconSize.width())
+ defaultTabIconHeight = max(defaultTabIconHeight, iconSize.height())
+ self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, icon, tabTitle)
+ defaultTabIconWidth = max(defaultTabIconWidth, 32)
+ defaultTabIconHeight = max(defaultTabIconHeight, 32)
+ self._tabWidget.setIconSize(QtCore.QSize(defaultTabIconWidth, defaultTabIconHeight))
+ self._tabWidget.currentChanged.connect(self._on_tab_changed)
+ self._tabWidget.setContentsMargins(0, 0, 0, 0)
+
+ self._layout.addWidget(self._tabWidget)
+
+ self._loginAction = QtGui.QAction(None)
+ self._loginAction.setText("Login")
+ self._loginAction.triggered.connect(self._on_login_requested)
+
+ self._importAction = QtGui.QAction(None)
+ self._importAction.setText("Import")
+ self._importAction.triggered.connect(self._on_import)
+
+ self._accountAction = QtGui.QAction(None)
+ self._accountAction.setText("Account")
+ self._accountAction.triggered.connect(self._on_account)
+
+ self._refreshConnectionAction = QtGui.QAction(None)
+ self._refreshConnectionAction.setText("Refresh Connection")
+ self._refreshConnectionAction.setShortcut(QtGui.QKeySequence("CTRL+a"))
+ self._refreshConnectionAction.triggered.connect(self._on_refresh_connection)
+
+ self._refreshTabAction = QtGui.QAction(None)
+ self._refreshTabAction.setText("Refresh Tab")
+ self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
+ self._refreshTabAction.triggered.connect(self._on_refresh)
+
+ fileMenu = self._window.menuBar().addMenu("&File")
+ fileMenu.addAction(self._loginAction)
+ fileMenu.addAction(self._refreshTabAction)
+ fileMenu.addAction(self._refreshConnectionAction)
+
+ toolsMenu = self._window.menuBar().addMenu("&Tools")
+ toolsMenu.addAction(self._accountAction)
+ toolsMenu.addAction(self._importAction)
+ toolsMenu.addAction(self._app.aboutAction)
+
+ self._initialize_tab(self._tabWidget.currentIndex())
+ self.set_fullscreen(self._app.fullscreenAction.isChecked())
+ self.update_orientation(self._app.orientation)
+
+ def _init_call_handler(self):
+ if self._callHandler is not None:
+ return
+ import call_handler
+ self._callHandler = call_handler.MissedCallWatcher()
+ self._callHandler.callMissed.connect(self._voicemailRefreshDelay.start)
+
+ def set_default_credentials(self, username, password):
+ self._defaultCredentials = username, password
+
+ def get_default_credentials(self):
+ return self._defaultCredentials
+
+ def walk_children(self):
+ if self._smsEntryDialog is not None:
+ return (self._smsEntryDialog, )
+ else:
+ return ()
+
+ def start(self):
+ qwrappers.WindowWrapper.start(self)
+ assert self._session.state == self._session.LOGGEDOUT_STATE, "Initialization messed up"
+ if self._defaultCredentials != ("", ""):
+ username, password = self._defaultCredentials[0], self._defaultCredentials[1]
+ self._curentCredentials = username, password
+ self._session.login(username, password)
+ else:
+ self._prompt_for_login()
+
+ def close(self):
+ for diag in (
+ self._credentialsDialog,
+ self._accountDialog,
+ ):
+ if diag is not None:
+ diag.close()
+ for child in self.walk_children():
+ child.window.destroyed.disconnect(self._on_child_close)
+ child.window.closed.disconnect(self._on_child_close)
+ child.close()
+ self._window.close()
+
+ def destroy(self):
+ qwrappers.WindowWrapper.destroy(self)
+ if self._session.state != self._session.LOGGEDOUT_STATE:
+ self._session.logout()
+
+ def get_current_tab(self):
+ return self._currentTab
+
+ def set_current_tab(self, tabIndex):
+ self._tabWidget.setCurrentIndex(tabIndex)
+
+ def load_settings(self, config):
+ blobs = "", ""
+ isFullscreen = False
+ orientation = self._app.orientation
+ tabIndex = 0
+ try:
+ blobs = [
+ config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
+ for i in xrange(len(self.get_default_credentials()))
+ ]
+ isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
+ tabIndex = config.getint(constants.__pretty_app_name__, "tab")
+ orientation = config.get(constants.__pretty_app_name__, "orientation")
+ except ConfigParser.NoOptionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing option %s" % (
+ constants._user_settings_,
+ e.option,
+ ),
+ )
+ except ConfigParser.NoSectionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+
+ try:
+ self._app.alarmHandler.load_settings(config, "alarm")
+ self._app.notifyOnMissed = config.getboolean("2 - Account Info", "notifyOnMissed")
+ self._app.notifyOnVoicemail = config.getboolean("2 - Account Info", "notifyOnVoicemail")
+ self._app.notifyOnSms = config.getboolean("2 - Account Info", "notifyOnSms")
+ self._updateVoicemailOnMissedCall = config.getboolean("2 - Account Info", "updateVoicemailOnMissedCall")
+ except ConfigParser.NoOptionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing option %s" % (
+ constants._user_settings_,
+ e.option,
+ ),
+ )
+ except ConfigParser.NoSectionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+
+ creds = (
+ base64.b64decode(blob)
+ for blob in blobs
+ )
+ self.set_default_credentials(*creds)
+ self._app.fullscreenAction.setChecked(isFullscreen)
+ self._app.set_orientation(orientation)
+ self.set_current_tab(tabIndex)
+
+ backendId = 2 # For backwards compatibility
+ for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
+ sectionName = "%s - %s" % (backendId, tabTitle)
+ settings = self._tabsContents[tabIndex].get_settings()
+ for settingName in settings.iterkeys():
+ try:
+ settingValue = config.get(sectionName, settingName)
+ except ConfigParser.NoOptionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ return
+ except ConfigParser.NoSectionError, e:
+ _moduleLogger.info(
+ "Settings file %s is missing section %s" % (
+ constants._user_settings_,
+ e.section,
+ ),
+ )
+ return
+ except Exception:
+ _moduleLogger.exception("Unknown loading error")
+ return
+ settings[settingName] = settingValue
+ self._tabsContents[tabIndex].set_settings(settings)
+
+ def save_settings(self, config):
+ config.add_section(constants.__pretty_app_name__)
+ config.set(constants.__pretty_app_name__, "tab", str(self.get_current_tab()))
+ config.set(constants.__pretty_app_name__, "fullscreen", str(self._app.fullscreenAction.isChecked()))
+ config.set(constants.__pretty_app_name__, "orientation", str(self._app.orientation))
+ for i, value in enumerate(self.get_default_credentials()):
+ blob = base64.b64encode(value)
+ config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
+
+ config.add_section("alarm")
+ self._app.alarmHandler.save_settings(config, "alarm")
+ config.add_section("2 - Account Info")
+ config.set("2 - Account Info", "notifyOnMissed", repr(self._app.notifyOnMissed))
+ config.set("2 - Account Info", "notifyOnVoicemail", repr(self._app.notifyOnVoicemail))
+ config.set("2 - Account Info", "notifyOnSms", repr(self._app.notifyOnSms))
+ config.set("2 - Account Info", "updateVoicemailOnMissedCall", repr(self._updateVoicemailOnMissedCall))
+
+ backendId = 2 # For backwards compatibility
+ for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
+ sectionName = "%s - %s" % (backendId, tabTitle)
+ config.add_section(sectionName)
+ tabSettings = self._tabsContents[tabIndex].get_settings()
+ for settingName, settingValue in tabSettings.iteritems():
+ config.set(sectionName, settingName, settingValue)
+
+ def update_orientation(self, orientation):
+ qwrappers.WindowWrapper.update_orientation(self, orientation)
+ windowOrientation = self.idealWindowOrientation
+ if windowOrientation == QtCore.Qt.Horizontal:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+ else:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+
+ def _initialize_tab(self, index):
+ assert index < self.MAX_TABS, "Invalid tab"
+ if not self._tabsContents[index].has_child():
+ tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
+ self._tabsContents[index].set_child(tab)
+ self._tabsContents[index].refresh(force=False)
+
+ def _prompt_for_login(self):
+ if self._credentialsDialog is None:
+ import dialogs
+ self._credentialsDialog = dialogs.CredentialsDialog(self._app)
+ credentials = self._credentialsDialog.run(
+ self._defaultCredentials[0], self._defaultCredentials[1], self.window
+ )
+ if credentials is None:
+ return
+ username, password = credentials
+ self._curentCredentials = username, password
+ self._session.login(username, password)
+
+ def _show_account_dialog(self):
+ if self._accountDialog is None:
+ import dialogs
+ self._accountDialog = dialogs.AccountDialog(self._window, self._app, self._app.errorLog)
+ self._accountDialog.setIfNotificationsSupported(self._app.alarmHandler.backgroundNotificationsSupported)
+ self._accountDialog.settingsApproved.connect(self._on_settings_approved)
+
+ if self._callHandler is not None and not self._callHandler.isSupported:
+ self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED
+ elif self._updateVoicemailOnMissedCall:
+ self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_ENABLED
+ else:
+ self._accountDialog.updateVMOnMissedCall = self._accountDialog.VOICEMAIL_CHECK_DISABLED
+ self._accountDialog.notifications = self._app.alarmHandler.alarmType
+ self._accountDialog.notificationTime = self._app.alarmHandler.recurrence
+ self._accountDialog.notifyOnMissed = self._app.notifyOnMissed
+ self._accountDialog.notifyOnVoicemail = self._app.notifyOnVoicemail
+ self._accountDialog.notifyOnSms = self._app.notifyOnSms
+ self._accountDialog.set_callbacks(
+ self._session.get_callback_numbers(), self._session.get_callback_number()
+ )
+ accountNumberToDisplay = self._session.get_account_number()
+ if not accountNumberToDisplay:
+ accountNumberToDisplay = "Not Available (%s)" % self._session.state
+ self._accountDialog.set_account_number(accountNumberToDisplay)
+ self._accountDialog.orientation = self._app.orientation
+
+ self._accountDialog.run()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_settings_approved(self):
+ if self._accountDialog.doClear:
+ self._session.logout_and_clear()
+ self._defaultCredentials = "", ""
+ self._curentCredentials = "", ""
+ for tab in self._tabsContents:
+ tab.disable()
+ else:
+ callbackNumber = self._accountDialog.selectedCallback
+ self._session.set_callback_number(callbackNumber)
+
+ if self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_NOT_SUPPORTED:
+ pass
+ elif self._accountDialog.updateVMOnMissedCall == self._accountDialog.VOICEMAIL_CHECK_ENABLED:
+ self._updateVoicemailOnMissedCall = True
+ self._init_call_handler()
+ self._callHandler.start()
+ else:
+ self._updateVoicemailOnMissedCall = False
+ if self._callHandler is not None:
+ self._callHandler.stop()
+ if (
+ self._accountDialog.notifyOnMissed or
+ self._accountDialog.notifyOnVoicemail or
+ self._accountDialog.notifyOnSms
+ ):
+ notifications = self._accountDialog.notifications
+ else:
+ notifications = self._accountDialog.ALARM_NONE
+ self._app.alarmHandler.apply_settings(notifications, self._accountDialog.notificationTime)
+
+ self._app.notifyOnMissed = self._accountDialog.notifyOnMissed
+ self._app.notifyOnVoicemail = self._accountDialog.notifyOnVoicemail
+ self._app.notifyOnSms = self._accountDialog.notifyOnSms
+ self._app.set_orientation(self._accountDialog.orientation)
+ self._app.save_settings()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_window_resized(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ windowOrientation = self.idealWindowOrientation
+ if windowOrientation == QtCore.Qt.Horizontal:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
+ else:
+ self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_new_message_alert(self):
+ with qui_utils.notify_error(self._errorLog):
+ if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+ if self._currentTab == self.MESSAGES_TAB or not self._app.ledHandler.isReal:
+ self._errorLog.push_message("New messages")
+ else:
+ self._app.ledHandler.on()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_missed(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force=True)
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_session_error(self, message):
+ with qui_utils.notify_error(self._errorLog):
+ self._errorLog.push_error(message)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_login(self):
+ with qui_utils.notify_error(self._errorLog):
+ changedAccounts = self._defaultCredentials != self._curentCredentials
+ noCallback = not self._session.get_callback_number()
+ if changedAccounts or noCallback:
+ self._show_account_dialog()
+
+ self._defaultCredentials = self._curentCredentials
+
+ for tab in self._tabsContents:
+ tab.enable()
+ self._initialize_tab(self._currentTab)
+ if self._updateVoicemailOnMissedCall:
+ self._init_call_handler()
+ self._callHandler.start()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_logout(self):
+ with qui_utils.notify_error(self._errorLog):
+ for tab in self._tabsContents:
+ tab.disable()
+ if self._callHandler is not None:
+ self._callHandler.stop()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_app_alert(self):
+ with qui_utils.notify_error(self._errorLog):
+ if self._session.state == self._session.LOGGEDIN_STATE:
+ messageType = {
+ (True, True): self._session.MESSAGE_ALL,
+ (True, False): self._session.MESSAGE_TEXTS,
+ (False, True): self._session.MESSAGE_VOICEMAILS,
+ }[(self._app.notifyOnSms, self._app.notifyOnVoicemail)]
+ self._session.update_messages(messageType, force=True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_recipients_changed(self):
+ with qui_utils.notify_error(self._errorLog):
+ if self._session.draft.get_num_contacts() == 0:
+ return
+
+ if self._smsEntryDialog is None:
+ import dialogs
+ self._smsEntryDialog = dialogs.SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
+ self._smsEntryDialog.window.destroyed.connect(self._on_child_close)
+ self._smsEntryDialog.window.closed.connect(self._on_child_close)
+ self._smsEntryDialog.window.show()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_child_close(self, obj = None):
+ self._smsEntryDialog = None
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_login_requested(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._prompt_for_login()
+
+ @qt_compat.Slot(int)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_tab_changed(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ self._currentTab = index
+ self._initialize_tab(index)
+ if self._app.alarmHandler.alarmType == self._app.alarmHandler.ALARM_APPLICATION:
+ self._app.ledHandler.off()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._tabsContents[self._currentTab].refresh(force=True)
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_connection(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ self._session.refresh_connection()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_import(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
+ csvName = unicode(csvName)
+ if not csvName:
+ return
+ import shutil
+ shutil.copy2(csvName, self._app.fsContactsPath)
+ if self._tabsContents[self.CONTACTS_TAB].has_child:
+ self._tabsContents[self.CONTACTS_TAB].child.update_addressbooks()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_account(self, checked = True):
+ with qui_utils.notify_error(self._errorLog):
+ assert self._session.state == self._session.LOGGEDIN_STATE, "Must be logged in for settings"
+ self._show_account_dialog()
+
+
+def run():
+ try:
+ os.makedirs(constants._data_path_)
+ except OSError, e:
+ if e.errno != 17:
+ raise
+
+ logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
+ logging.basicConfig(level=logging.DEBUG, format=logFormat)
+ rotating = logging.handlers.RotatingFileHandler(constants._user_logpath_, maxBytes=512*1024, backupCount=1)
+ rotating.setFormatter(logging.Formatter(logFormat))
+ root = logging.getLogger()
+ root.addHandler(rotating)
+ _moduleLogger.info("%s %s-%s" % (constants.__app_name__, constants.__version__, constants.__build__))
+ _moduleLogger.info("OS: %s" % (os.uname()[0], ))
+ _moduleLogger.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+ _moduleLogger.info("Hostname: %s" % os.uname()[1])
+
+ try:
+ import gobject
+ gobject.threads_init()
+ except ImportError:
+ _moduleLogger.info("GObject support not available")
+ try:
+ import dbus
+ try:
+ from dbus.mainloop.qt import DBusQtMainLoop
+ DBusQtMainLoop(set_as_default=True)
+ _moduleLogger.info("Using Qt mainloop")
+ except ImportError:
+ try:
+ from dbus.mainloop.glib import DBusGMainLoop
+ DBusGMainLoop(set_as_default=True)
+ _moduleLogger.info("Using GObject mainloop")
+ except ImportError:
+ _moduleLogger.info("Mainloop not available")
+ except ImportError:
+ _moduleLogger.info("DBus support not available")
+
+ app = QtGui.QApplication([])
+ handle = Dialcentral(app)
+ qtpie.init_pies()
+ return app.exec_()
+
+
+if __name__ == "__main__":
+ import sys
+
+ val = run()
+ sys.exit(val)
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import functools
+import copy
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+import constants
+from util import qwrappers
+from util import qui_utils
+from util import misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class CredentialsDialog(object):
+
+ def __init__(self, app):
+ self._app = app
+ self._usernameField = QtGui.QLineEdit()
+ self._passwordField = QtGui.QLineEdit()
+ self._passwordField.setEchoMode(QtGui.QLineEdit.Password)
+
+ self._credLayout = QtGui.QGridLayout()
+ self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
+ self._credLayout.addWidget(self._usernameField, 0, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
+ self._credLayout.addWidget(self._passwordField, 1, 1)
+
+ self._loginButton = QtGui.QPushButton("&Login")
+ self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
+ self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._credLayout)
+ self._layout.addWidget(self._buttonLayout)
+
+ self._dialog = QtGui.QDialog()
+ self._dialog.setWindowTitle("Login")
+ self._dialog.setLayout(self._layout)
+ self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
+ self._buttonLayout.accepted.connect(self._dialog.accept)
+ self._buttonLayout.rejected.connect(self._dialog.reject)
+
+ self._closeWindowAction = QtGui.QAction(None)
+ self._closeWindowAction.setText("Close")
+ self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+ self._closeWindowAction.triggered.connect(self._on_close_window)
+
+ self._dialog.addAction(self._closeWindowAction)
+ self._dialog.addAction(app.quitAction)
+ self._dialog.addAction(app.fullscreenAction)
+
+ def run(self, defaultUsername, defaultPassword, parent=None):
+ self._dialog.setParent(parent, QtCore.Qt.Dialog)
+ try:
+ self._usernameField.setText(defaultUsername)
+ self._passwordField.setText(defaultPassword)
+
+ response = self._dialog.exec_()
+ if response == QtGui.QDialog.Accepted:
+ return str(self._usernameField.text()), str(self._passwordField.text())
+ elif response == QtGui.QDialog.Rejected:
+ return None
+ else:
+ _moduleLogger.error("Unknown response")
+ return None
+ finally:
+ self._dialog.setParent(None, QtCore.Qt.Dialog)
+
+ def close(self):
+ try:
+ self._dialog.reject()
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._dialog.reject()
+
+
+class AboutDialog(object):
+
+ def __init__(self, app):
+ self._app = app
+ self._title = QtGui.QLabel(
+ "<h1>%s</h1><h3>Version: %s</h3>" % (
+ constants.__pretty_app_name__, constants.__version__
+ )
+ )
+ self._title.setTextFormat(QtCore.Qt.RichText)
+ self._title.setAlignment(QtCore.Qt.AlignCenter)
+ self._copyright = QtGui.QLabel("<h6>Developed by Ed Page<h6><h6>Icons: See website</h6>")
+ self._copyright.setTextFormat(QtCore.Qt.RichText)
+ self._copyright.setAlignment(QtCore.Qt.AlignCenter)
+ self._link = QtGui.QLabel('<a href="http://gc-dialer.garage.maemo.org">DialCentral Website</a>')
+ self._link.setTextFormat(QtCore.Qt.RichText)
+ self._link.setAlignment(QtCore.Qt.AlignCenter)
+ self._link.setOpenExternalLinks(True)
+
+ self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addWidget(self._title)
+ self._layout.addWidget(self._copyright)
+ self._layout.addWidget(self._link)
+ self._layout.addWidget(self._buttonLayout)
+
+ self._dialog = QtGui.QDialog()
+ self._dialog.setWindowTitle("About")
+ self._dialog.setLayout(self._layout)
+ self._buttonLayout.rejected.connect(self._dialog.reject)
+
+ self._closeWindowAction = QtGui.QAction(None)
+ self._closeWindowAction.setText("Close")
+ self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
+ self._closeWindowAction.triggered.connect(self._on_close_window)
+
+ self._dialog.addAction(self._closeWindowAction)
+ self._dialog.addAction(app.quitAction)
+ self._dialog.addAction(app.fullscreenAction)
+
+ def run(self, parent=None):
+ self._dialog.setParent(parent, QtCore.Qt.Dialog)
+
+ response = self._dialog.exec_()
+ return response
+
+ def close(self):
+ try:
+ self._dialog.reject()
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._dialog.reject()
+
+
+class AccountDialog(QtCore.QObject, qwrappers.WindowWrapper):
+
+ # @bug Can't enter custom callback numbers
+
+ _RECURRENCE_CHOICES = [
+ (1, "1 minute"),
+ (2, "2 minutes"),
+ (3, "3 minutes"),
+ (5, "5 minutes"),
+ (8, "8 minutes"),
+ (10, "10 minutes"),
+ (15, "15 minutes"),
+ (30, "30 minutes"),
+ (45, "45 minutes"),
+ (60, "1 hour"),
+ (3*60, "3 hours"),
+ (6*60, "6 hours"),
+ (12*60, "12 hours"),
+ ]
+
+ ALARM_NONE = "No Alert"
+ ALARM_BACKGROUND = "Background Alert"
+ ALARM_APPLICATION = "Application Alert"
+
+ VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported"
+ VOICEMAIL_CHECK_DISABLED = "Disabled"
+ VOICEMAIL_CHECK_ENABLED = "Enabled"
+
+ settingsApproved = qt_compat.Signal()
+
+ def __init__(self, parent, app, errorLog):
+ QtCore.QObject.__init__(self)
+ qwrappers.WindowWrapper.__init__(self, parent, app)
+ self._app = app
+ self._doClear = False
+
+ self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
+ self._notificationSelecter = QtGui.QComboBox()
+ self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change)
+ self._notificationTimeSelector = QtGui.QComboBox()
+ #self._notificationTimeSelector.setEditable(True)
+ self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
+ for _, label in self._RECURRENCE_CHOICES:
+ self._notificationTimeSelector.addItem(label)
+ self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
+ self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
+ self._smsNotificationButton = QtGui.QCheckBox("SMS")
+ self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls")
+ self._clearButton = QtGui.QPushButton("Clear Account")
+ self._clearButton.clicked.connect(self._on_clear)
+ self._callbackSelector = QtGui.QComboBox()
+ #self._callbackSelector.setEditable(True)
+ self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
+ self._orientationSelector = QtGui.QComboBox()
+ for orientationMode in [
+ self._app.DEFAULT_ORIENTATION,
+ self._app.AUTO_ORIENTATION,
+ self._app.LANDSCAPE_ORIENTATION,
+ self._app.PORTRAIT_ORIENTATION,
+ ]:
+ self._orientationSelector.addItem(orientationMode)
+
+ self._update_notification_state()
+
+ self._credLayout = QtGui.QGridLayout()
+ self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
+ self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
+ self._credLayout.addWidget(self._callbackSelector, 1, 1)
+ self._credLayout.addWidget(self._notificationSelecter, 2, 0)
+ self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
+ self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 4, 0)
+ self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
+ self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Other"), 6, 0)
+ self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1)
+ self._credLayout.addWidget(QtGui.QLabel("Orientation"), 7, 0)
+ self._credLayout.addWidget(self._orientationSelector, 7, 1)
+ self._credLayout.addWidget(QtGui.QLabel(""), 8, 0)
+ self._credLayout.addWidget(QtGui.QLabel(""), 9, 0)
+ self._credLayout.addWidget(self._clearButton, 9, 1)
+
+ self._credWidget = QtGui.QWidget()
+ self._credWidget.setLayout(self._credLayout)
+ self._credWidget.setContentsMargins(0, 0, 0, 0)
+ self._scrollSettings = QtGui.QScrollArea()
+ self._scrollSettings.setWidget(self._credWidget)
+ self._scrollSettings.setWidgetResizable(True)
+ self._scrollSettings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self._scrollSettings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+ self._applyButton = QtGui.QPushButton("&Apply")
+ self._applyButton.clicked.connect(self._on_settings_apply)
+ self._cancelButton = QtGui.QPushButton("&Cancel")
+ self._cancelButton.clicked.connect(self._on_settings_cancel)
+ self._buttonLayout = QtGui.QHBoxLayout()
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._cancelButton)
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._applyButton)
+ self._buttonLayout.addStretch()
+
+ self._layout.addWidget(self._scrollSettings)
+ self._layout.addLayout(self._buttonLayout)
+ self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
+
+ self._window.setWindowTitle("Account")
+ self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
+
+ @property
+ def doClear(self):
+ return self._doClear
+
+ def setIfNotificationsSupported(self, isSupported):
+ if isSupported:
+ self._notificationSelecter.clear()
+ self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND])
+ self._notificationTimeSelector.setEnabled(False)
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(False)
+ self._smsNotificationButton.setEnabled(False)
+ else:
+ self._notificationSelecter.clear()
+ self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION])
+ self._notificationTimeSelector.setEnabled(False)
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(False)
+ self._smsNotificationButton.setEnabled(False)
+
+ def set_account_number(self, num):
+ self._accountNumberLabel.setText(num)
+
+ orientation = property(
+ lambda self: str(self._orientationSelector.currentText()),
+ lambda self, mode: qui_utils.set_current_index(self._orientationSelector, mode),
+ )
+
+ def _set_voicemail_on_missed(self, status):
+ if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED:
+ self._voicemailOnMissedButton.setChecked(False)
+ self._voicemailOnMissedButton.hide()
+ elif status == self.VOICEMAIL_CHECK_DISABLED:
+ self._voicemailOnMissedButton.setChecked(False)
+ self._voicemailOnMissedButton.show()
+ elif status == self.VOICEMAIL_CHECK_ENABLED:
+ self._voicemailOnMissedButton.setChecked(True)
+ self._voicemailOnMissedButton.show()
+ else:
+ raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status)
+
+ def _get_voicemail_on_missed(self):
+ if not self._voicemailOnMissedButton.isVisible():
+ return self.VOICEMAIL_CHECK_NOT_SUPPORTED
+ elif self._voicemailOnMissedButton.isChecked():
+ return self.VOICEMAIL_CHECK_ENABLED
+ else:
+ return self.VOICEMAIL_CHECK_DISABLED
+
+ updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed)
+
+ notifications = property(
+ lambda self: str(self._notificationSelecter.currentText()),
+ lambda self, enabled: qui_utils.set_current_index(self._notificationSelecter, enabled),
+ )
+
+ notifyOnMissed = property(
+ lambda self: self._missedCallsNotificationButton.isChecked(),
+ lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled),
+ )
+
+ notifyOnVoicemail = property(
+ lambda self: self._voicemailNotificationButton.isChecked(),
+ lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled),
+ )
+
+ notifyOnSms = property(
+ lambda self: self._smsNotificationButton.isChecked(),
+ lambda self, enabled: self._smsNotificationButton.setChecked(enabled),
+ )
+
+ def _get_notification_time(self):
+ index = self._notificationTimeSelector.currentIndex()
+ minutes = self._RECURRENCE_CHOICES[index][0]
+ return minutes
+
+ def _set_notification_time(self, minutes):
+ for i, (time, _) in enumerate(self._RECURRENCE_CHOICES):
+ if time == minutes:
+ self._notificationTimeSelector.setCurrentIndex(i)
+ break
+ else:
+ self._notificationTimeSelector.setCurrentIndex(0)
+
+ notificationTime = property(_get_notification_time, _set_notification_time)
+
+ @property
+ def selectedCallback(self):
+ index = self._callbackSelector.currentIndex()
+ data = str(self._callbackSelector.itemData(index))
+ return data
+
+ def set_callbacks(self, choices, default):
+ self._callbackSelector.clear()
+
+ self._callbackSelector.addItem("Not Set", "")
+
+ uglyDefault = misc_utils.make_ugly(default)
+ if not uglyDefault:
+ uglyDefault = default
+ for number, description in choices.iteritems():
+ prettyNumber = misc_utils.make_pretty(number)
+ uglyNumber = misc_utils.make_ugly(number)
+ if not uglyNumber:
+ prettyNumber = number
+ uglyNumber = number
+
+ self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
+ if uglyNumber == uglyDefault:
+ self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
+
+ def run(self):
+ self._doClear = False
+ self._window.show()
+
+ def close(self):
+ try:
+ self._window.hide()
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ def _update_notification_state(self):
+ currentText = str(self._notificationSelecter.currentText())
+ if currentText == self.ALARM_BACKGROUND:
+ self._notificationTimeSelector.setEnabled(True)
+
+ self._missedCallsNotificationButton.setEnabled(True)
+ self._voicemailNotificationButton.setEnabled(True)
+ self._smsNotificationButton.setEnabled(True)
+ elif currentText == self.ALARM_APPLICATION:
+ self._notificationTimeSelector.setEnabled(True)
+
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(True)
+ self._smsNotificationButton.setEnabled(True)
+
+ self._missedCallsNotificationButton.setChecked(False)
+ else:
+ self._notificationTimeSelector.setEnabled(False)
+
+ self._missedCallsNotificationButton.setEnabled(False)
+ self._voicemailNotificationButton.setEnabled(False)
+ self._smsNotificationButton.setEnabled(False)
+
+ self._missedCallsNotificationButton.setChecked(False)
+ self._voicemailNotificationButton.setChecked(False)
+ self._smsNotificationButton.setChecked(False)
+
+ @qt_compat.Slot(int)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_notification_change(self, index):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_notification_state()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_settings_cancel(self, checked = False):
+ with qui_utils.notify_error(self._app.errorLog):
+ self.hide()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ def _on_settings_apply(self, checked = False):
+ self.__on_settings_apply(checked)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def __on_settings_apply(self, checked = False):
+ with qui_utils.notify_error(self._app.errorLog):
+ self.settingsApproved.emit()
+ self.hide()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_clear(self, checked = False):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._doClear = True
+ self.settingsApproved.emit()
+ self.hide()
+
+
+class ContactList(object):
+
+ _SENTINEL_ICON = QtGui.QIcon()
+
+ def __init__(self, app, session):
+ self._app = app
+ self._session = session
+ self._targetLayout = QtGui.QVBoxLayout()
+ self._targetList = QtGui.QWidget()
+ self._targetList.setLayout(self._targetLayout)
+ self._uiItems = []
+ self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
+
+ @property
+ def toplevel(self):
+ return self._targetList
+
+ def setVisible(self, isVisible):
+ self._targetList.setVisible(isVisible)
+
+ def update(self):
+ cids = list(self._session.draft.get_contacts())
+ amountCommon = min(len(cids), len(self._uiItems))
+
+ # Run through everything in common
+ for i in xrange(0, amountCommon):
+ cid = cids[i]
+ uiItem = self._uiItems[i]
+ title = self._session.draft.get_title(cid)
+ description = self._session.draft.get_description(cid)
+ numbers = self._session.draft.get_numbers(cid)
+ uiItem["cid"] = cid
+ uiItem["title"] = title
+ uiItem["description"] = description
+ uiItem["numbers"] = numbers
+ uiItem["label"].setText(title)
+ self._populate_number_selector(uiItem["selector"], cid, i, numbers)
+ uiItem["rowWidget"].setVisible(True)
+
+ # More contacts than ui items
+ for i in xrange(amountCommon, len(cids)):
+ cid = cids[i]
+ title = self._session.draft.get_title(cid)
+ description = self._session.draft.get_description(cid)
+ numbers = self._session.draft.get_numbers(cid)
+
+ titleLabel = QtGui.QLabel(title)
+ titleLabel.setWordWrap(True)
+ numberSelector = QtGui.QComboBox()
+ self._populate_number_selector(numberSelector, cid, i, numbers)
+
+ callback = functools.partial(
+ self._on_change_number,
+ i
+ )
+ callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
+ numberSelector.activated.connect(
+ qt_compat.Slot(int)(callback)
+ )
+
+ if self._closeIcon is self._SENTINEL_ICON:
+ deleteButton = QtGui.QPushButton("Delete")
+ else:
+ deleteButton = QtGui.QPushButton(self._closeIcon, "")
+ deleteButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ callback = functools.partial(
+ self._on_remove_contact,
+ i
+ )
+ callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
+ deleteButton.clicked.connect(callback)
+
+ rowLayout = QtGui.QHBoxLayout()
+ rowLayout.addWidget(titleLabel, 1000)
+ rowLayout.addWidget(numberSelector, 0)
+ rowLayout.addWidget(deleteButton, 0)
+ rowWidget = QtGui.QWidget()
+ rowWidget.setLayout(rowLayout)
+ self._targetLayout.addWidget(rowWidget)
+
+ uiItem = {}
+ uiItem["cid"] = cid
+ uiItem["title"] = title
+ uiItem["description"] = description
+ uiItem["numbers"] = numbers
+ uiItem["label"] = titleLabel
+ uiItem["selector"] = numberSelector
+ uiItem["rowWidget"] = rowWidget
+ self._uiItems.append(uiItem)
+ amountCommon = i+1
+
+ # More UI items than contacts
+ for i in xrange(amountCommon, len(self._uiItems)):
+ uiItem = self._uiItems[i]
+ uiItem["rowWidget"].setVisible(False)
+ amountCommon = i+1
+
+ def _populate_number_selector(self, selector, cid, cidIndex, numbers):
+ selector.clear()
+
+ selectedNumber = self._session.draft.get_selected_number(cid)
+ if len(numbers) == 1:
+ # If no alt numbers available, check the address book
+ numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
+ else:
+ defaultIndex = _index_number(numbers, selectedNumber)
+
+ for number, description in numbers:
+ if description:
+ label = "%s - %s" % (number, description)
+ else:
+ label = number
+ selector.addItem(label)
+ selector.setVisible(True)
+ if 1 < len(numbers):
+ selector.setEnabled(True)
+ selector.setCurrentIndex(defaultIndex)
+ else:
+ selector.setEnabled(False)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_change_number(self, cidIndex, index):
+ with qui_utils.notify_error(self._app.errorLog):
+ # Exception thrown when the first item is removed
+ try:
+ cid = self._uiItems[cidIndex]["cid"]
+ numbers = self._session.draft.get_numbers(cid)
+ except IndexError:
+ _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
+ return
+ except KeyError:
+ _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
+ return
+ number = numbers[index][0]
+ self._session.draft.set_selected_number(cid, number)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_remove_contact(self, index, toggled):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._session.draft.remove_contact(self._uiItems[index]["cid"])
+
+
+class VoicemailPlayer(object):
+
+ def __init__(self, app, session, errorLog):
+ self._app = app
+ self._session = session
+ self._errorLog = errorLog
+ self._token = None
+ self._session.voicemailAvailable.connect(self._on_voicemail_downloaded)
+ self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+
+ self._playButton = QtGui.QPushButton("Play")
+ self._playButton.clicked.connect(self._on_voicemail_play)
+ self._pauseButton = QtGui.QPushButton("Pause")
+ self._pauseButton.clicked.connect(self._on_voicemail_pause)
+ self._pauseButton.hide()
+ self._resumeButton = QtGui.QPushButton("Resume")
+ self._resumeButton.clicked.connect(self._on_voicemail_resume)
+ self._resumeButton.hide()
+ self._stopButton = QtGui.QPushButton("Stop")
+ self._stopButton.clicked.connect(self._on_voicemail_stop)
+ self._stopButton.hide()
+
+ self._downloadButton = QtGui.QPushButton("Download Voicemail")
+ self._downloadButton.clicked.connect(self._on_voicemail_download)
+ self._downloadLayout = QtGui.QHBoxLayout()
+ self._downloadLayout.addWidget(self._downloadButton)
+ self._downloadWidget = QtGui.QWidget()
+ self._downloadWidget.setLayout(self._downloadLayout)
+
+ self._playLabel = QtGui.QLabel("Voicemail")
+ self._saveButton = QtGui.QPushButton("Save")
+ self._saveButton.clicked.connect(self._on_voicemail_save)
+ self._playerLayout = QtGui.QHBoxLayout()
+ self._playerLayout.addWidget(self._playLabel)
+ self._playerLayout.addWidget(self._playButton)
+ self._playerLayout.addWidget(self._pauseButton)
+ self._playerLayout.addWidget(self._resumeButton)
+ self._playerLayout.addWidget(self._stopButton)
+ self._playerLayout.addWidget(self._saveButton)
+ self._playerWidget = QtGui.QWidget()
+ self._playerWidget.setLayout(self._playerLayout)
+
+ self._visibleWidget = None
+ self._layout = QtGui.QHBoxLayout()
+ self._layout.setContentsMargins(0, 0, 0, 0)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+ self._update_state()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def destroy(self):
+ self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded)
+ self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
+ self._invalidate_token()
+
+ def _invalidate_token(self):
+ if self._token is not None:
+ self._token.invalidate()
+ self._token.error.disconnect(self._on_play_error)
+ self._token.stateChange.connect(self._on_play_state)
+ self._token.invalidated.connect(self._on_play_invalidated)
+
+ def _show_download(self, messageId):
+ if self._visibleWidget is self._downloadWidget:
+ return
+ self._hide()
+ self._layout.addWidget(self._downloadWidget)
+ self._visibleWidget = self._downloadWidget
+ self._visibleWidget.show()
+
+ def _show_player(self, messageId):
+ if self._visibleWidget is self._playerWidget:
+ return
+ self._hide()
+ self._layout.addWidget(self._playerWidget)
+ self._visibleWidget = self._playerWidget
+ self._visibleWidget.show()
+
+ def _hide(self):
+ if self._visibleWidget is None:
+ return
+ self._visibleWidget.hide()
+ self._layout.removeWidget(self._visibleWidget)
+ self._visibleWidget = None
+
+ def _update_play_state(self):
+ if self._token is not None and self._token.isValid:
+ self._playButton.setText("Stop")
+ else:
+ self._playButton.setText("Play")
+
+ def _update_state(self):
+ if self._session.draft.get_num_contacts() != 1:
+ self._hide()
+ return
+
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ if messageId is None:
+ self._hide()
+ return
+
+ if self._session.is_available(messageId):
+ self._show_player(messageId)
+ else:
+ self._show_download(messageId)
+ if self._token is not None:
+ self._token.invalidate()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_save(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)")
+ targetPath = unicode(targetPath)
+ if not targetPath:
+ return
+
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ sourcePath = self._session.voicemail_path(messageId)
+ import shutil
+ shutil.copy2(sourcePath, targetPath)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_play_error(self, error):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._app.errorLog.push_error(error)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_play_invalidated(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._playButton.show()
+ self._pauseButton.hide()
+ self._resumeButton.hide()
+ self._stopButton.hide()
+ self._invalidate_token()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_play_state(self, state):
+ with qui_utils.notify_error(self._app.errorLog):
+ if state == self._token.STATE_PLAY:
+ self._playButton.hide()
+ self._pauseButton.show()
+ self._resumeButton.hide()
+ self._stopButton.show()
+ elif state == self._token.STATE_PAUSE:
+ self._playButton.hide()
+ self._pauseButton.hide()
+ self._resumeButton.show()
+ self._stopButton.show()
+ elif state == self._token.STATE_STOP:
+ self._playButton.show()
+ self._pauseButton.hide()
+ self._resumeButton.hide()
+ self._stopButton.hide()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_play(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ sourcePath = self._session.voicemail_path(messageId)
+
+ self._invalidate_token()
+ uri = "file://%s" % sourcePath
+ self._token = self._app.streamHandler.set_file(uri)
+ self._token.stateChange.connect(self._on_play_state)
+ self._token.invalidated.connect(self._on_play_invalidated)
+ self._token.error.connect(self._on_play_error)
+ self._token.play()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_pause(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._token.pause()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_resume(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._token.play()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_stop(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._token.stop()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_download(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ (cid, ) = self._session.draft.get_contacts()
+ messageId = self._session.draft.get_message_id(cid)
+ self._session.download_voicemail(messageId)
+ self._hide()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_recipients_changed(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_state()
+
+ @qt_compat.Slot(str, str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_voicemail_downloaded(self, messageId, filepath):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_state()
+
+
+class SMSEntryWindow(qwrappers.WindowWrapper):
+
+ MAX_CHAR = 160
+ # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
+
+ def __init__(self, parent, app, session, errorLog):
+ qwrappers.WindowWrapper.__init__(self, parent, app)
+ self._session = session
+ self._session.messagesUpdated.connect(self._on_refresh_history)
+ self._session.historyUpdated.connect(self._on_refresh_history)
+ self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
+
+ self._session.draft.sendingMessage.connect(self._on_op_started)
+ self._session.draft.calling.connect(self._on_op_started)
+ self._session.draft.calling.connect(self._on_calling_started)
+ self._session.draft.cancelling.connect(self._on_op_started)
+
+ self._session.draft.sentMessage.connect(self._on_op_finished)
+ self._session.draft.called.connect(self._on_op_finished)
+ self._session.draft.cancelled.connect(self._on_op_finished)
+ self._session.draft.error.connect(self._on_op_error)
+
+ self._errorLog = errorLog
+
+ self._targetList = ContactList(self._app, self._session)
+ self._history = QtGui.QLabel()
+ self._history.setTextFormat(QtCore.Qt.RichText)
+ self._history.setWordWrap(True)
+ self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog)
+ self._smsEntry = QtGui.QTextEdit()
+ self._smsEntry.textChanged.connect(self._on_letter_count_changed)
+
+ self._entryLayout = QtGui.QVBoxLayout()
+ self._entryLayout.addWidget(self._targetList.toplevel)
+ self._entryLayout.addWidget(self._history)
+ self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0)
+ self._entryLayout.addWidget(self._smsEntry)
+ self._entryLayout.setContentsMargins(0, 0, 0, 0)
+ self._entryWidget = QtGui.QWidget()
+ self._entryWidget.setLayout(self._entryLayout)
+ self._entryWidget.setContentsMargins(0, 0, 0, 0)
+ self._scrollEntry = QtGui.QScrollArea()
+ self._scrollEntry.setWidget(self._entryWidget)
+ self._scrollEntry.setWidgetResizable(True)
+ self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
+ self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+ self._characterCountLabel = QtGui.QLabel("")
+ self._singleNumberSelector = QtGui.QComboBox()
+ self._cids = []
+ self._singleNumberSelector.activated.connect(self._on_single_change_number)
+ self._smsButton = QtGui.QPushButton("SMS")
+ self._smsButton.clicked.connect(self._on_sms_clicked)
+ self._smsButton.setEnabled(False)
+ self._dialButton = QtGui.QPushButton("Dial")
+ self._dialButton.clicked.connect(self._on_call_clicked)
+ self._cancelButton = QtGui.QPushButton("Cancel Call")
+ self._cancelButton.clicked.connect(self._on_cancel_clicked)
+ self._cancelButton.setVisible(False)
+
+ self._buttonLayout = QtGui.QHBoxLayout()
+ self._buttonLayout.addWidget(self._characterCountLabel)
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._singleNumberSelector)
+ self._buttonLayout.addStretch()
+ self._buttonLayout.addWidget(self._smsButton)
+ self._buttonLayout.addWidget(self._dialButton)
+ self._buttonLayout.addWidget(self._cancelButton)
+
+ self._layout.addWidget(self._errorDisplay.toplevel)
+ self._layout.addWidget(self._scrollEntry)
+ self._layout.addLayout(self._buttonLayout)
+ self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
+
+ self._window.setWindowTitle("Contact")
+ self._window.closed.connect(self._on_close_window)
+ self._window.hidden.connect(self._on_close_window)
+ self._window.resized.connect(self._on_window_resized)
+
+ self._scrollTimer = QtCore.QTimer()
+ self._scrollTimer.setInterval(100)
+ self._scrollTimer.setSingleShot(True)
+ self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
+
+ self._smsEntry.setPlainText(self._session.draft.message)
+ self._update_letter_count()
+ self._update_target_fields()
+ self.set_fullscreen(self._app.fullscreenAction.isChecked())
+ self.update_orientation(self._app.orientation)
+
+ def close(self):
+ if self._window is None:
+ # Already closed
+ return
+ window = self._window
+ try:
+ message = unicode(self._smsEntry.toPlainText())
+ self._session.draft.message = message
+ self.hide()
+ except AttributeError:
+ _moduleLogger.exception("Oh well")
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ def destroy(self):
+ self._session.messagesUpdated.disconnect(self._on_refresh_history)
+ self._session.historyUpdated.disconnect(self._on_refresh_history)
+ self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
+ self._session.draft.sendingMessage.disconnect(self._on_op_started)
+ self._session.draft.calling.disconnect(self._on_op_started)
+ self._session.draft.calling.disconnect(self._on_calling_started)
+ self._session.draft.cancelling.disconnect(self._on_op_started)
+ self._session.draft.sentMessage.disconnect(self._on_op_finished)
+ self._session.draft.called.disconnect(self._on_op_finished)
+ self._session.draft.cancelled.disconnect(self._on_op_finished)
+ self._session.draft.error.disconnect(self._on_op_error)
+ self._voicemailPlayer.destroy()
+ window = self._window
+ self._window = None
+ try:
+ window.close()
+ window.destroy()
+ except AttributeError:
+ _moduleLogger.exception("Oh well")
+ except RuntimeError:
+ _moduleLogger.exception("Oh well")
+
+ def update_orientation(self, orientation):
+ qwrappers.WindowWrapper.update_orientation(self, orientation)
+ self._scroll_to_bottom()
+
+ def _update_letter_count(self):
+ count = len(self._smsEntry.toPlainText())
+ numTexts, numCharInText = divmod(count, self.MAX_CHAR)
+ numTexts += 1
+ numCharsLeftInText = self.MAX_CHAR - numCharInText
+ self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
+
+ def _update_button_state(self):
+ self._cancelButton.setEnabled(True)
+ if self._session.draft.get_num_contacts() == 0:
+ self._dialButton.setEnabled(False)
+ self._smsButton.setEnabled(False)
+ elif self._session.draft.get_num_contacts() == 1:
+ count = len(self._smsEntry.toPlainText())
+ if count == 0:
+ self._dialButton.setEnabled(True)
+ self._smsButton.setEnabled(False)
+ else:
+ self._dialButton.setEnabled(False)
+ self._smsButton.setEnabled(True)
+ else:
+ self._dialButton.setEnabled(False)
+ count = len(self._smsEntry.toPlainText())
+ if count == 0:
+ self._smsButton.setEnabled(False)
+ else:
+ self._smsButton.setEnabled(True)
+
+ def _update_history(self, cid):
+ draftContactsCount = self._session.draft.get_num_contacts()
+ if draftContactsCount != 1:
+ self._history.setVisible(False)
+ else:
+ description = self._session.draft.get_description(cid)
+
+ self._targetList.setVisible(False)
+ if description:
+ self._history.setText(description)
+ self._history.setVisible(True)
+ else:
+ self._history.setText("")
+ self._history.setVisible(False)
+
+ def _update_target_fields(self):
+ draftContactsCount = self._session.draft.get_num_contacts()
+ if draftContactsCount == 0:
+ self.hide()
+ del self._cids[:]
+ elif draftContactsCount == 1:
+ (cid, ) = self._session.draft.get_contacts()
+ title = self._session.draft.get_title(cid)
+ numbers = self._session.draft.get_numbers(cid)
+
+ self._targetList.setVisible(False)
+ self._update_history(cid)
+ self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
+ self._cids = [cid]
+
+ self._scroll_to_bottom()
+ self._window.setWindowTitle(title)
+ self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
+ self.show()
+ self._window.raise_()
+ else:
+ self._targetList.setVisible(True)
+ self._targetList.update()
+ self._history.setText("")
+ self._history.setVisible(False)
+ self._singleNumberSelector.setVisible(False)
+
+ self._scroll_to_bottom()
+ self._window.setWindowTitle("Contacts")
+ self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
+ self.show()
+ self._window.raise_()
+
+ def _populate_number_selector(self, selector, cid, cidIndex, numbers):
+ selector.clear()
+
+ selectedNumber = self._session.draft.get_selected_number(cid)
+ if len(numbers) == 1:
+ # If no alt numbers available, check the address book
+ numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
+ else:
+ defaultIndex = _index_number(numbers, selectedNumber)
+
+ for number, description in numbers:
+ if description:
+ label = "%s - %s" % (number, description)
+ else:
+ label = number
+ selector.addItem(label)
+ selector.setVisible(True)
+ if 1 < len(numbers):
+ selector.setEnabled(True)
+ selector.setCurrentIndex(defaultIndex)
+ else:
+ selector.setEnabled(False)
+
+ def _scroll_to_bottom(self):
+ self._scrollTimer.start()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_delayed_scroll_to_bottom(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._scrollEntry.ensureWidgetVisible(self._smsEntry)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_sms_clicked(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ message = unicode(self._smsEntry.toPlainText())
+ self._session.draft.message = message
+ self._session.draft.send()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_clicked(self, arg):
+ with qui_utils.notify_error(self._app.errorLog):
+ message = unicode(self._smsEntry.toPlainText())
+ self._session.draft.message = message
+ self._session.draft.call()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_cancel_clicked(self, message):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._session.draft.cancel()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_single_change_number(self, index):
+ with qui_utils.notify_error(self._app.errorLog):
+ # Exception thrown when the first item is removed
+ cid = self._cids[0]
+ try:
+ numbers = self._session.draft.get_numbers(cid)
+ except KeyError:
+ _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
+ return
+ number = numbers[index][0]
+ self._session.draft.set_selected_number(cid, number)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_history(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ draftContactsCount = self._session.draft.get_num_contacts()
+ if draftContactsCount != 1:
+ # Changing contact count will automatically refresh it
+ return
+ (cid, ) = self._session.draft.get_contacts()
+ self._update_history(cid)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_recipients_changed(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_target_fields()
+ self._update_button_state()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_started(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._smsEntry.setReadOnly(True)
+ self._smsButton.setVisible(False)
+ self._dialButton.setVisible(False)
+ self.show()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_calling_started(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._cancelButton.setVisible(True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_finished(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._smsEntry.setPlainText("")
+ self._smsEntry.setReadOnly(False)
+ self._cancelButton.setVisible(False)
+ self._smsButton.setVisible(True)
+ self._dialButton.setVisible(True)
+ self.close()
+ self.destroy()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_op_error(self, message):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._smsEntry.setReadOnly(False)
+ self._cancelButton.setVisible(False)
+ self._smsButton.setVisible(True)
+ self._dialButton.setVisible(True)
+
+ self._errorLog.push_error(message)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_letter_count_changed(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._update_letter_count()
+ self._update_button_state()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_window_resized(self):
+ with qui_utils.notify_error(self._app.errorLog):
+ self._scroll_to_bottom()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_close_window(self, checked = True):
+ with qui_utils.notify_error(self._app.errorLog):
+ self.close()
+
+
+def _index_number(numbers, default):
+ uglyDefault = misc_utils.make_ugly(default)
+ uglyContactNumbers = list(
+ misc_utils.make_ugly(contactNumber)
+ for (contactNumber, _) in numbers
+ )
+ defaultMatches = [
+ misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
+ for contactNumber in uglyContactNumbers
+ ]
+ try:
+ defaultIndex = defaultMatches.index(True)
+ except ValueError:
+ defaultIndex = -1
+ _moduleLogger.warn(
+ "Could not find contact number %s among %r" % (
+ default, numbers
+ )
+ )
+ return defaultIndex
+
+
+def _get_contact_numbers(session, contactId, number, description):
+ contactPhoneNumbers = []
+ if contactId and contactId != "0":
+ try:
+ contactDetails = copy.deepcopy(session.get_contacts()[contactId])
+ contactPhoneNumbers = contactDetails["numbers"]
+ except KeyError:
+ contactPhoneNumbers = []
+ contactPhoneNumbers = [
+ (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
+ for contactPhoneNumber in contactPhoneNumbers
+ ]
+ defaultIndex = _index_number(contactPhoneNumbers, number)
+
+ if not contactPhoneNumbers or defaultIndex == -1:
+ contactPhoneNumbers += [(number, description)]
+ defaultIndex = 0
+
+ return contactPhoneNumbers, defaultIndex
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+
+import sys
+import datetime
+import ConfigParser
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import constants
+import alarm_notify
+
+
+def notify_on_change():
+ with open(constants._notifier_logpath_, "a") as file:
+ file.write("Notification: %r\n" % (datetime.datetime.now(), ))
+
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ backend = alarm_notify.create_backend(config)
+ notifyUser = alarm_notify.is_changed(config, backend)
+
+ if notifyUser:
+ file.write("\tChange occurred\n")
+
+
+if __name__ == "__main__":
+ notify_on_change()
--- /dev/null
+#!/usr/bin/env python
+
+import os
+import sys
+import ConfigParser
+import logging
+
+
+sys.path.insert(0,"/usr/lib/dialcentral/")
+
+
+import constants
+import alarm_notify
+
+
+def notify_on_change():
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._user_settings_)
+ backend = alarm_notify.create_backend(config)
+ notifyUser = alarm_notify.is_changed(config, backend)
+
+ config = ConfigParser.SafeConfigParser()
+ config.read(constants._custom_notifier_settings_)
+ soundFile = config.get("Sound Notifier", "soundfile")
+ soundFile = "/usr/lib/gv-notifier/alert.mp3"
+
+ if notifyUser:
+ import subprocess
+ import led_handler
+ logging.info("Changed, playing %s" % soundFile)
+ led = led_handler.LedHandler()
+ led.on()
+ soundOn = subprocess.call("/usr/bin/dbus-send --dest=com.nokia.osso_media_server --print-reply /com/nokia/osso_media_server com.nokia.osso_media_server.music.play_media string:file://%s",shell=True)
+ else:
+ logging.info("No Change")
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.WARNING, filename=constants._notifier_logpath_)
+ logging.info("Sound Notifier %s-%s" % (constants.__version__, constants.__build__))
+ logging.info("OS: %s" % (os.uname()[0], ))
+ logging.info("Kernel: %s (%s) for %s" % os.uname()[2:])
+ logging.info("Hostname: %s" % os.uname()[1])
+ try:
+ notify_on_change()
+ except:
+ logging.exception("Error")
+ raise
--- /dev/null
+#!/usr/bin/env python
+
+from __future__ import with_statement
+from __future__ import division
+
+import datetime
+import string
+import itertools
+import logging
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+QtGui = qt_compat.import_module("QtGui")
+
+from util import qtpie
+from util import qui_utils
+from util import misc as misc_utils
+
+import backends.null_backend as null_backend
+import backends.file_backend as file_backend
+import backends.qt_backend as qt_backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+_SENTINEL_ICON = QtGui.QIcon()
+
+
+class Dialpad(object):
+
+ def __init__(self, app, session, errorLog):
+ self._app = app
+ self._session = session
+ self._errorLog = errorLog
+
+ self._plus = QtGui.QPushButton("+")
+ self._plus.clicked.connect(lambda: self._on_keypress("+"))
+ self._entry = QtGui.QLineEdit()
+
+ backAction = QtGui.QAction(None)
+ backAction.setText("Back")
+ backAction.triggered.connect(self._on_backspace)
+ backPieItem = qtpie.QActionPieItem(backAction)
+ clearAction = QtGui.QAction(None)
+ clearAction.setText("Clear")
+ clearAction.triggered.connect(self._on_clear_text)
+ clearPieItem = qtpie.QActionPieItem(clearAction)
+ backSlices = [
+ qtpie.PieFiling.NULL_CENTER,
+ clearPieItem,
+ qtpie.PieFiling.NULL_CENTER,
+ qtpie.PieFiling.NULL_CENTER,
+ ]
+ self._back = qtpie.QPieButton(backPieItem)
+ self._back.set_center(backPieItem)
+ for slice in backSlices:
+ self._back.insertItem(slice)
+
+ self._entryLayout = QtGui.QHBoxLayout()
+ self._entryLayout.addWidget(self._plus, 1, QtCore.Qt.AlignCenter)
+ self._entryLayout.addWidget(self._entry, 1000)
+ self._entryLayout.addWidget(self._back, 1, QtCore.Qt.AlignCenter)
+
+ smsIcon = self._app.get_icon("messages.png")
+ self._smsButton = QtGui.QPushButton(smsIcon, "SMS")
+ self._smsButton.clicked.connect(self._on_sms_clicked)
+ self._smsButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ callIcon = self._app.get_icon("dialpad.png")
+ self._callButton = QtGui.QPushButton(callIcon, "Call")
+ self._callButton.clicked.connect(self._on_call_clicked)
+ self._callButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+
+ self._padLayout = QtGui.QGridLayout()
+ rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
+ columns = [0, 1, 2] * 3
+ keys = [
+ ("1", ""),
+ ("2", "ABC"),
+ ("3", "DEF"),
+ ("4", "GHI"),
+ ("5", "JKL"),
+ ("6", "MNO"),
+ ("7", "PQRS"),
+ ("8", "TUV"),
+ ("9", "WXYZ"),
+ ]
+ for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
+ self._padLayout.addWidget(self._generate_key_button(num, letters), row, column)
+ self._zerothButton = QtGui.QPushButton("0")
+ self._zerothButton.clicked.connect(lambda: self._on_keypress("0"))
+ self._zerothButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ self._padLayout.addWidget(self._smsButton, 3, 0)
+ self._padLayout.addWidget(self._zerothButton)
+ self._padLayout.addWidget(self._callButton, 3, 2)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._entryLayout, 0)
+ self._layout.addLayout(self._padLayout, 1000000)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._smsButton.setEnabled(True)
+ self._callButton.setEnabled(True)
+
+ def disable(self):
+ self._smsButton.setEnabled(False)
+ self._callButton.setEnabled(False)
+
+ def get_settings(self):
+ return {}
+
+ def set_settings(self, settings):
+ pass
+
+ def clear(self):
+ pass
+
+ def refresh(self, force = True):
+ pass
+
+ def _generate_key_button(self, center, letters):
+ button = QtGui.QPushButton("%s\n%s" % (center, letters))
+ button.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.MinimumExpanding,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ button.clicked.connect(lambda: self._on_keypress(center))
+ return button
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_keypress(self, key):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.insert(key)
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_backspace(self, toggled = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.backspace()
+
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_clear_text(self, toggled = False):
+ with qui_utils.notify_error(self._errorLog):
+ self._entry.clear()
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_sms_clicked(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ number = misc_utils.make_ugly(str(self._entry.text()))
+ self._entry.clear()
+
+ contactId = number
+ title = misc_utils.make_pretty(number)
+ description = misc_utils.make_pretty(number)
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+
+ @qt_compat.Slot()
+ @qt_compat.Slot(bool)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_call_clicked(self, checked = False):
+ with qui_utils.notify_error(self._errorLog):
+ number = misc_utils.make_ugly(str(self._entry.text()))
+ self._entry.clear()
+
+ contactId = number
+ title = misc_utils.make_pretty(number)
+ description = misc_utils.make_pretty(number)
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.clear()
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+ self._session.draft.call()
+
+
+class TimeCategories(object):
+
+ _NOW_SECTION = 0
+ _TODAY_SECTION = 1
+ _WEEK_SECTION = 2
+ _MONTH_SECTION = 3
+ _REST_SECTION = 4
+ _MAX_SECTIONS = 5
+
+ _NO_ELAPSED = datetime.timedelta(hours=1)
+ _WEEK_ELAPSED = datetime.timedelta(weeks=1)
+ _MONTH_ELAPSED = datetime.timedelta(days=30)
+
+ def __init__(self, parentItem):
+ self._timeItems = [
+ QtGui.QStandardItem(description)
+ for (i, description) in zip(
+ xrange(self._MAX_SECTIONS),
+ ["Now", "Today", "Week", "Month", "Past"],
+ )
+ ]
+ for item in self._timeItems:
+ item.setEditable(False)
+ item.setCheckable(False)
+ row = (item, )
+ parentItem.appendRow(row)
+
+ self._today = datetime.datetime(1900, 1, 1)
+
+ self.prepare_for_update(self._today)
+
+ def prepare_for_update(self, newToday):
+ self._today = newToday
+ for item in self._timeItems:
+ item.removeRows(0, item.rowCount())
+ try:
+ hour = self._today.strftime("%X")
+ day = self._today.strftime("%x")
+ except ValueError:
+ _moduleLogger.exception("Can't format times")
+ hour = "Now"
+ day = "Today"
+ self._timeItems[self._NOW_SECTION].setText(hour)
+ self._timeItems[self._TODAY_SECTION].setText(day)
+
+ def add_row(self, rowDate, row):
+ elapsedTime = self._today - rowDate
+ todayTuple = self._today.timetuple()
+ rowTuple = rowDate.timetuple()
+ if elapsedTime < self._NO_ELAPSED:
+ section = self._NOW_SECTION
+ elif todayTuple[0:3] == rowTuple[0:3]:
+ section = self._TODAY_SECTION
+ elif elapsedTime < self._WEEK_ELAPSED:
+ section = self._WEEK_SECTION
+ elif elapsedTime < self._MONTH_ELAPSED:
+ section = self._MONTH_SECTION
+ else:
+ section = self._REST_SECTION
+ self._timeItems[section].appendRow(row)
+
+ def get_item(self, timeIndex, rowIndex, column):
+ timeItem = self._timeItems[timeIndex]
+ item = timeItem.child(rowIndex, column)
+ return item
+
+
+class History(object):
+
+ DETAILS_IDX = 0
+ FROM_IDX = 1
+ MAX_IDX = 2
+
+ HISTORY_RECEIVED = "Received"
+ HISTORY_MISSED = "Missed"
+ HISTORY_PLACED = "Placed"
+ HISTORY_ALL = "All"
+
+ HISTORY_ITEM_TYPES = [HISTORY_RECEIVED, HISTORY_MISSED, HISTORY_PLACED, HISTORY_ALL]
+ HISTORY_COLUMNS = ["", "From"]
+ assert len(HISTORY_COLUMNS) == MAX_IDX
+
+ def __init__(self, app, session, errorLog):
+ self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
+ self._app = app
+ self._session = session
+ self._session.historyUpdated.connect(self._on_history_updated)
+ self._errorLog = errorLog
+
+ self._typeSelection = QtGui.QComboBox()
+ self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
+ self._typeSelection.setCurrentIndex(
+ self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
+ )
+ self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
+ refreshIcon = qui_utils.get_theme_icon(
+ ("view-refresh", "general_refresh", "gtk-refresh", ),
+ _SENTINEL_ICON
+ )
+ if refreshIcon is not _SENTINEL_ICON:
+ self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+ else:
+ self._refreshButton = QtGui.QPushButton("Refresh")
+ self._refreshButton.clicked.connect(self._on_refresh_clicked)
+ self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ self._managerLayout = QtGui.QHBoxLayout()
+ self._managerLayout.addWidget(self._typeSelection, 1000)
+ self._managerLayout.addWidget(self._refreshButton, 0)
+
+ self._itemStore = QtGui.QStandardItemModel()
+ self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
+ self._categoryManager = TimeCategories(self._itemStore)
+
+ self._itemView = QtGui.QTreeView()
+ self._itemView.setModel(self._itemStore)
+ self._itemView.setUniformRowHeights(True)
+ self._itemView.setRootIsDecorated(False)
+ self._itemView.setIndentation(0)
+ self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+ self._itemView.setHeaderHidden(True)
+ self._itemView.setItemsExpandable(False)
+ self._itemView.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
+ self._itemView.activated.connect(self._on_row_activated)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._managerLayout)
+ self._layout.addWidget(self._itemView)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self._actionIcon = {
+ "Placed": self._app.get_icon("placed.png"),
+ "Missed": self._app.get_icon("missed.png"),
+ "Received": self._app.get_icon("received.png"),
+ }
+
+ self._populate_items()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._itemView.setEnabled(True)
+
+ def disable(self):
+ self._itemView.setEnabled(False)
+
+ def get_settings(self):
+ return {
+ "filter": self._selectedFilter,
+ }
+
+ def set_settings(self, settings):
+ selectedFilter = settings.get("filter", self.HISTORY_ITEM_TYPES[-1])
+ if selectedFilter in self.HISTORY_ITEM_TYPES:
+ self._selectedFilter = selectedFilter
+ self._typeSelection.setCurrentIndex(
+ self.HISTORY_ITEM_TYPES.index(selectedFilter)
+ )
+
+ def clear(self):
+ self._itemView.clear()
+
+ def refresh(self, force=True):
+ self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
+
+ if self._selectedFilter == self.HISTORY_RECEIVED:
+ self._session.update_history(self._session.HISTORY_RECEIVED, force)
+ elif self._selectedFilter == self.HISTORY_MISSED:
+ self._session.update_history(self._session.HISTORY_MISSED, force)
+ elif self._selectedFilter == self.HISTORY_PLACED:
+ self._session.update_history(self._session.HISTORY_PLACED, force)
+ elif self._selectedFilter == self.HISTORY_ALL:
+ self._session.update_history(self._session.HISTORY_ALL, force)
+ else:
+ assert False, "How did we get here?"
+
+ if self._app.notifyOnMissed and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
+ self._app.ledHandler.off()
+
+ def _populate_items(self):
+ self._categoryManager.prepare_for_update(self._session.get_when_history_updated())
+
+ history = self._session.get_history()
+ history.sort(key=lambda item: item["time"], reverse=True)
+ for event in history:
+ if self._selectedFilter not in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
+ continue
+
+ relTime = event["relTime"]
+ action = event["action"]
+ number = event["number"]
+ prettyNumber = misc_utils.make_pretty(number)
+ if prettyNumber.startswith("+1 "):
+ prettyNumber = prettyNumber[len("+1 "):]
+ name = event["name"]
+ if not name or name == number:
+ name = event["location"]
+ if not name:
+ name = "Unknown"
+
+ detailsItem = QtGui.QStandardItem(self._actionIcon[action], "%s\n%s" % (prettyNumber, relTime))
+ detailsFont = detailsItem.font()
+ detailsFont.setPointSize(max(detailsFont.pointSize() - 6, 5))
+ detailsItem.setFont(detailsFont)
+ nameItem = QtGui.QStandardItem(name)
+ nameFont = nameItem.font()
+ nameFont.setPointSize(nameFont.pointSize() + 4)
+ nameItem.setFont(nameFont)
+ row = detailsItem, nameItem
+ for item in row:
+ item.setEditable(False)
+ item.setCheckable(False)
+ row[self.DETAILS_IDX].setData(event)
+ self._categoryManager.add_row(event["time"], row)
+ self._itemView.expandAll()
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._selectedFilter = str(newItem)
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_history_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_clicked(self, arg = None):
+ with qui_utils.notify_error(self._errorLog):
+ self.refresh(force=True)
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ timeIndex = index.parent()
+ if not timeIndex.isValid():
+ return
+ timeRow = timeIndex.row()
+ row = index.row()
+ detailsItem = self._categoryManager.get_item(timeRow, row, self.DETAILS_IDX)
+ fromItem = self._categoryManager.get_item(timeRow, row, self.FROM_IDX)
+ contactDetails = detailsItem.data()
+
+ title = unicode(fromItem.text())
+ number = str(contactDetails["number"])
+ contactId = number # ids don't seem too unique so using numbers
+
+ descriptionRows = []
+ for t in xrange(self._itemStore.rowCount()):
+ randomTimeItem = self._itemStore.item(t, 0)
+ for i in xrange(randomTimeItem.rowCount()):
+ iItem = randomTimeItem.child(i, 0)
+ iContactDetails = iItem.data()
+ iNumber = str(iContactDetails["number"])
+ if number != iNumber:
+ continue
+ relTime = misc_utils.abbrev_relative_date(iContactDetails["relTime"])
+ action = str(iContactDetails["action"])
+ number = str(iContactDetails["number"])
+ prettyNumber = misc_utils.make_pretty(number)
+ rowItems = relTime, action, prettyNumber
+ descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
+ description = "<table>%s</table>" % "".join(descriptionRows)
+ numbersWithDescriptions = [(str(contactDetails["number"]), "")]
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+
+
+class Messages(object):
+
+ NO_MESSAGES = "None"
+ VOICEMAIL_MESSAGES = "Voicemail"
+ TEXT_MESSAGES = "SMS"
+ ALL_TYPES = "All Messages"
+ MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
+
+ UNREAD_STATUS = "Unread"
+ UNARCHIVED_STATUS = "Inbox"
+ ALL_STATUS = "Any"
+ MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
+
+ _MIN_MESSAGES_SHOWN = 1
+
+ def __init__(self, app, session, errorLog):
+ self._selectedTypeFilter = self.ALL_TYPES
+ self._selectedStatusFilter = self.ALL_STATUS
+ self._app = app
+ self._session = session
+ self._session.messagesUpdated.connect(self._on_messages_updated)
+ self._errorLog = errorLog
+
+ self._typeSelection = QtGui.QComboBox()
+ self._typeSelection.addItems(self.MESSAGE_TYPES)
+ self._typeSelection.setCurrentIndex(
+ self.MESSAGE_TYPES.index(self._selectedTypeFilter)
+ )
+ self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
+
+ self._statusSelection = QtGui.QComboBox()
+ self._statusSelection.addItems(self.MESSAGE_STATUSES)
+ self._statusSelection.setCurrentIndex(
+ self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
+ )
+ self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
+
+ refreshIcon = qui_utils.get_theme_icon(
+ ("view-refresh", "general_refresh", "gtk-refresh", ),
+ _SENTINEL_ICON
+ )
+ if refreshIcon is not _SENTINEL_ICON:
+ self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+ else:
+ self._refreshButton = QtGui.QPushButton("Refresh")
+ self._refreshButton.clicked.connect(self._on_refresh_clicked)
+ self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+
+ self._selectionLayout = QtGui.QHBoxLayout()
+ self._selectionLayout.addWidget(self._typeSelection, 1000)
+ self._selectionLayout.addWidget(self._statusSelection, 1000)
+ self._selectionLayout.addWidget(self._refreshButton, 0)
+
+ self._itemStore = QtGui.QStandardItemModel()
+ self._itemStore.setHorizontalHeaderLabels(["Messages"])
+ self._categoryManager = TimeCategories(self._itemStore)
+
+ self._htmlDelegate = qui_utils.QHtmlDelegate()
+ self._itemView = QtGui.QTreeView()
+ self._itemView.setModel(self._itemStore)
+ self._itemView.setUniformRowHeights(False)
+ self._itemView.setRootIsDecorated(False)
+ self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+ self._itemView.setHeaderHidden(True)
+ self._itemView.setItemsExpandable(False)
+ self._itemView.setItemDelegate(self._htmlDelegate)
+ self._itemView.activated.connect(self._on_row_activated)
+ self._itemView.header().sectionResized.connect(self._on_column_resized)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._selectionLayout)
+ self._layout.addWidget(self._itemView)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self._populate_items()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._itemView.setEnabled(True)
+
+ def disable(self):
+ self._itemView.setEnabled(False)
+
+ def get_settings(self):
+ return {
+ "type": self._selectedTypeFilter,
+ "status": self._selectedStatusFilter,
+ }
+
+ def set_settings(self, settings):
+ selectedType = settings.get("type", self.ALL_TYPES)
+ if selectedType in self.MESSAGE_TYPES:
+ self._selectedTypeFilter = selectedType
+ self._typeSelection.setCurrentIndex(
+ self.MESSAGE_TYPES.index(self._selectedTypeFilter)
+ )
+
+ selectedStatus = settings.get("status", self.ALL_STATUS)
+ if selectedStatus in self.MESSAGE_STATUSES:
+ self._selectedStatusFilter = selectedStatus
+ self._statusSelection.setCurrentIndex(
+ self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
+ )
+
+ def clear(self):
+ self._itemView.clear()
+
+ def refresh(self, force=True):
+ self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
+
+ if self._selectedTypeFilter == self.NO_MESSAGES:
+ pass
+ elif self._selectedTypeFilter == self.TEXT_MESSAGES:
+ self._session.update_messages(self._session.MESSAGE_TEXTS, force)
+ elif self._selectedTypeFilter == self.VOICEMAIL_MESSAGES:
+ self._session.update_messages(self._session.MESSAGE_VOICEMAILS, force)
+ elif self._selectedTypeFilter == self.ALL_TYPES:
+ self._session.update_messages(self._session.MESSAGE_ALL, force)
+ else:
+ assert False, "How did we get here?"
+
+ if (self._app.notifyOnSms or self._app.notifyOnVoicemail) and self._app.alarmHandler.alarmType != self._app.alarmHandler.ALARM_NONE:
+ self._app.ledHandler.off()
+
+ def _populate_items(self):
+ self._categoryManager.prepare_for_update(self._session.get_when_messages_updated())
+
+ rawMessages = self._session.get_messages()
+ rawMessages.sort(key=lambda item: item["time"], reverse=True)
+ for item in rawMessages:
+ isUnarchived = not item["isArchived"]
+ isUnread = not item["isRead"]
+ visibleStatus = {
+ self.UNREAD_STATUS: isUnarchived and isUnread,
+ self.UNARCHIVED_STATUS: isUnarchived,
+ self.ALL_STATUS: True,
+ }[self._selectedStatusFilter]
+ visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
+ if not (visibleType and visibleStatus):
+ continue
+
+ relTime = misc_utils.abbrev_relative_date(item["relTime"])
+ number = item["number"]
+ prettyNumber = misc_utils.make_pretty(number)
+ name = item["name"]
+ if not name or name == number:
+ name = item["location"]
+ if not name:
+ name = "Unknown"
+
+ messageParts = list(item["messageParts"])
+ if len(messageParts) == 0:
+ messages = ("No Transcription", )
+ elif len(messageParts) == 1:
+ if messageParts[0][1]:
+ messages = (messageParts[0][1], )
+ else:
+ messages = ("No Transcription", )
+ else:
+ messages = [
+ "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
+ for messagePart in messageParts
+ ]
+
+ firstMessage = "<b>%s<br/>%s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
+
+ expandedMessages = [firstMessage]
+ expandedMessages.extend(messages)
+ if self._MIN_MESSAGES_SHOWN < len(messages):
+ secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
+ collapsedMessages = [firstMessage, secondMessage]
+ collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
+ else:
+ collapsedMessages = expandedMessages
+
+ item = dict(item.iteritems())
+ item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
+ item["expandedMessages"] = "<br/>\n".join(expandedMessages)
+
+ messageItem = QtGui.QStandardItem(item["collapsedMessages"])
+ messageItem.setData(item)
+ messageItem.setEditable(False)
+ messageItem.setCheckable(False)
+ row = (messageItem, )
+ self._categoryManager.add_row(item["time"], row)
+ self._itemView.expandAll()
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_type_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._selectedTypeFilter = str(newItem)
+ self._populate_items()
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_status_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._selectedStatusFilter = str(newItem)
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_clicked(self, arg = None):
+ with qui_utils.notify_error(self._errorLog):
+ self.refresh(force=True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_messages_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ timeIndex = index.parent()
+ if not timeIndex.isValid():
+ return
+ timeRow = timeIndex.row()
+ row = index.row()
+ item = self._categoryManager.get_item(timeRow, row, 0)
+ contactDetails = item.data()
+
+ name = unicode(contactDetails["name"])
+ number = str(contactDetails["number"])
+ if not name or name == number:
+ name = unicode(contactDetails["location"])
+ if not name:
+ name = "Unknown"
+
+ if str(contactDetails["type"]) == "Voicemail":
+ messageId = str(contactDetails["id"])
+ else:
+ messageId = None
+ contactId = str(contactDetails["contactId"])
+ title = name
+ description = unicode(contactDetails["expandedMessages"])
+ numbersWithDescriptions = [(number, "")]
+ self._session.draft.add_contact(contactId, messageId, title, description, numbersWithDescriptions)
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_column_resized(self, index, oldSize, newSize):
+ self._htmlDelegate.setWidth(newSize, self._itemStore)
+
+
+class Contacts(object):
+
+ # @todo Provide some sort of letter jump
+
+ def __init__(self, app, session, errorLog):
+ self._app = app
+ self._session = session
+ self._session.accountUpdated.connect(self._on_contacts_updated)
+ self._errorLog = errorLog
+ self._addressBookFactories = [
+ null_backend.NullAddressBookFactory(),
+ file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
+ qt_backend.QtContactsAddressBookFactory(),
+ ]
+ self._addressBooks = []
+
+ self._listSelection = QtGui.QComboBox()
+ self._listSelection.addItems([])
+ self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
+ self._activeList = "None"
+ refreshIcon = qui_utils.get_theme_icon(
+ ("view-refresh", "general_refresh", "gtk-refresh", ),
+ _SENTINEL_ICON
+ )
+ if refreshIcon is not _SENTINEL_ICON:
+ self._refreshButton = QtGui.QPushButton(refreshIcon, "")
+ else:
+ self._refreshButton = QtGui.QPushButton("Refresh")
+ self._refreshButton.clicked.connect(self._on_refresh_clicked)
+ self._refreshButton.setSizePolicy(QtGui.QSizePolicy(
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.Minimum,
+ QtGui.QSizePolicy.PushButton,
+ ))
+ self._managerLayout = QtGui.QHBoxLayout()
+ self._managerLayout.addWidget(self._listSelection, 1000)
+ self._managerLayout.addWidget(self._refreshButton, 0)
+
+ self._itemStore = QtGui.QStandardItemModel()
+ self._itemStore.setHorizontalHeaderLabels(["Contacts"])
+ self._alphaItem = {}
+
+ self._itemView = QtGui.QTreeView()
+ self._itemView.setModel(self._itemStore)
+ self._itemView.setUniformRowHeights(True)
+ self._itemView.setRootIsDecorated(False)
+ self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+ self._itemView.setHeaderHidden(True)
+ self._itemView.setItemsExpandable(False)
+ self._itemView.activated.connect(self._on_row_activated)
+
+ self._layout = QtGui.QVBoxLayout()
+ self._layout.addLayout(self._managerLayout)
+ self._layout.addWidget(self._itemView)
+ self._widget = QtGui.QWidget()
+ self._widget.setLayout(self._layout)
+
+ self.update_addressbooks()
+ self._populate_items()
+
+ @property
+ def toplevel(self):
+ return self._widget
+
+ def enable(self):
+ self._itemView.setEnabled(True)
+
+ def disable(self):
+ self._itemView.setEnabled(False)
+
+ def get_settings(self):
+ return {
+ "selectedAddressbook": self._activeList,
+ }
+
+ def set_settings(self, settings):
+ currentItem = settings.get("selectedAddressbook", "None")
+ bookNames = [book["name"] for book in self._addressBooks]
+ try:
+ newIndex = bookNames.index(currentItem)
+ except ValueError:
+ # Switch over to None for the user
+ newIndex = 0
+ self._listSelection.setCurrentIndex(newIndex)
+ self._activeList = currentItem
+
+ def clear(self):
+ self._itemView.clear()
+
+ def refresh(self, force=True):
+ self._itemView.setFocus(QtCore.Qt.OtherFocusReason)
+ self._backend.update_account(force)
+
+ @property
+ def _backend(self):
+ return self._addressBooks[self._listSelection.currentIndex()]["book"]
+
+ def update_addressbooks(self):
+ self._addressBooks = [
+ {"book": book, "name": book.name}
+ for factory in self._addressBookFactories
+ for book in factory.get_addressbooks()
+ ]
+ self._addressBooks.append(
+ {
+ "book": self._session,
+ "name": "Google Voice",
+ }
+ )
+
+ currentItem = str(self._listSelection.currentText())
+ self._activeList = currentItem
+ if currentItem == "":
+ # Not loaded yet
+ currentItem = "None"
+ self._listSelection.clear()
+ bookNames = [book["name"] for book in self._addressBooks]
+ try:
+ newIndex = bookNames.index(currentItem)
+ except ValueError:
+ # Switch over to None for the user
+ newIndex = 0
+ self._itemStore.clear()
+ _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem)
+ self._listSelection.addItems(bookNames)
+ self._listSelection.setCurrentIndex(newIndex)
+
+ def _populate_items(self):
+ self._itemStore.clear()
+ self._alphaItem = dict(
+ (letter, QtGui.QStandardItem(letter))
+ for letter in self._prefixes()
+ )
+ for letter in self._prefixes():
+ item = self._alphaItem[letter]
+ item.setEditable(False)
+ item.setCheckable(False)
+ row = (item, )
+ self._itemStore.appendRow(row)
+
+ for item in self._get_contacts():
+ name = item["name"]
+ if not name:
+ name = "Unknown"
+ numbers = item["numbers"]
+
+ nameItem = QtGui.QStandardItem(name)
+ nameItem.setEditable(False)
+ nameItem.setCheckable(False)
+ nameItem.setData(item)
+ nameItemFont = nameItem.font()
+ nameItemFont.setPointSize(max(nameItemFont.pointSize() + 4, 5))
+ nameItem.setFont(nameItemFont)
+
+ row = (nameItem, )
+ rowKey = name[0].upper()
+ rowKey = rowKey if rowKey in self._alphaItem else "#"
+ self._alphaItem[rowKey].appendRow(row)
+ self._itemView.expandAll()
+
+ def _prefixes(self):
+ return itertools.chain(string.ascii_uppercase, ("#", ))
+
+ def _jump_to_prefix(self, letter):
+ i = list(self._prefixes()).index(letter)
+ rootIndex = self._itemView.rootIndex()
+ currentIndex = self._itemView.model().index(i, 0, rootIndex)
+ self._itemView.scrollTo(currentIndex)
+ self._itemView.setItemSelected(self._itemView.topLevelItem(i), True)
+
+ def _get_contacts(self):
+ contacts = list(self._backend.get_contacts().itervalues())
+ contacts.sort(key=lambda contact: contact["name"].lower())
+ return contacts
+
+ @qt_compat.Slot(str)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_filter_changed(self, newItem):
+ with qui_utils.notify_error(self._errorLog):
+ self._activeList = str(newItem)
+ self.refresh(force=False)
+ self._populate_items()
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_refresh_clicked(self, arg = None):
+ with qui_utils.notify_error(self._errorLog):
+ self.refresh(force=True)
+
+ @qt_compat.Slot()
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_contacts_updated(self):
+ with qui_utils.notify_error(self._errorLog):
+ self._populate_items()
+
+ @qt_compat.Slot(QtCore.QModelIndex)
+ @misc_utils.log_exception(_moduleLogger)
+ def _on_row_activated(self, index):
+ with qui_utils.notify_error(self._errorLog):
+ letterIndex = index.parent()
+ if not letterIndex.isValid():
+ return
+ letterRow = letterIndex.row()
+ letter = list(self._prefixes())[letterRow]
+ letterItem = self._alphaItem[letter]
+ rowIndex = index.row()
+ item = letterItem.child(rowIndex, 0)
+ contactDetails = item.data()
+
+ name = unicode(contactDetails["name"])
+ if not name:
+ name = unicode(contactDetails["location"])
+ if not name:
+ name = "Unknown"
+
+ contactId = str(contactDetails["contactId"])
+ numbers = contactDetails["numbers"]
+ numbers = [
+ dict(
+ (str(k), str(v))
+ for (k, v) in number.iteritems()
+ )
+ for number in numbers
+ ]
+ numbersWithDescriptions = [
+ (
+ number["phoneNumber"],
+ self._choose_phonetype(number),
+ )
+ for number in numbers
+ ]
+ title = name
+ description = name
+ self._session.draft.add_contact(contactId, None, title, description, numbersWithDescriptions)
+
+ @staticmethod
+ def _choose_phonetype(numberDetails):
+ if "phoneTypeName" in numberDetails:
+ return numberDetails["phoneTypeName"]
+ elif "phoneType" in numberDetails:
+ return numberDetails["phoneType"]
+ else:
+ return ""
--- /dev/null
+#!/usr/bin/env python
+
+import dbus
+
+
+class _NokiaLedHandler(object):
+
+ def __init__(self):
+ self._bus = dbus.SystemBus()
+ self._rawMceRequest = self._bus.get_object("com.nokia.mce", "/com/nokia/mce/request")
+ self._mceRequest = dbus.Interface(self._rawMceRequest, dbus_interface="com.nokia.mce.request")
+
+ self._ledPattern = "PatternCommunicationChat"
+
+ def on(self):
+ self._mceRequest.req_led_pattern_activate(self._ledPattern)
+
+ def off(self):
+ self._mceRequest.req_led_pattern_deactivate(self._ledPattern)
+
+
+class _NoLedHandler(object):
+
+ def __init__(self):
+ pass
+
+ def on(self):
+ pass
+
+ def off(self):
+ pass
+
+
+class LedHandler(object):
+
+ def __init__(self):
+ self._actual = None
+ self._isReal = False
+
+ def on(self):
+ self._lazy_init()
+ self._actual.on()
+
+ def off(self):
+ self._lazy_init()
+ self._actual.off()
+
+ @property
+ def isReal(self):
+ self._lazy_init()
+ self._isReal
+
+ def _lazy_init(self):
+ if self._actual is not None:
+ return
+ try:
+ self._actual = _NokiaLedHandler()
+ self._isReal = True
+ except dbus.DBusException:
+ self._actual = _NoLedHandler()
+ self._isReal = False
+
+
+if __name__ == "__main__":
+ leds = _NokiaLedHandler()
+ leds.off()
--- /dev/null
+from __future__ import with_statement
+
+import os
+import time
+import datetime
+import contextlib
+import logging
+
+try:
+ import cPickle
+ pickle = cPickle
+except ImportError:
+ import pickle
+
+import util.qt_compat as qt_compat
+QtCore = qt_compat.QtCore
+
+from util import qore_utils
+from util import qui_utils
+from util import concurrent
+from util import misc as misc_utils
+
+import constants
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class _DraftContact(object):
+
+ def __init__(self, messageId, title, description, numbersWithDescriptions):
+ self.messageId = messageId
+ self.title = title
+ self.description = description
+ self.numbers = numbersWithDescriptions
+ self.selectedNumber = numbersWithDescriptions[0][0]
+
+
+class Draft(QtCore.QObject):
+
+ sendingMessage = qt_compat.Signal()
+ sentMessage = qt_compat.Signal()
+ calling = qt_compat.Signal()
+ called = qt_compat.Signal()
+ cancelling = qt_compat.Signal()
+ cancelled = qt_compat.Signal()
+ error = qt_compat.Signal(str)
+
+ recipientsChanged = qt_compat.Signal()
+
+ def __init__(self, asyncQueue, backend, errorLog):
+ QtCore.QObject.__init__(self)
+ self._errorLog = errorLog
+ self._contacts = {}
+ self._asyncQueue = asyncQueue
+ self._backend = backend
+ self._busyReason = None
+ self._message = ""
+
+ def send(self):
+ assert 0 < len(self._contacts), "No contacts selected"
+ assert 0 < len(self._message), "No message to send"
+ numbers = [misc_utils.make_ugly(contact.selectedNumber) for contact in self._contacts.itervalues()]
+ le = self._asyncQueue.add_async(self._send)
+ le.start(numbers, self._message)
+
+ def call(self):
+ assert len(self._contacts) == 1, "Must select 1 and only 1 contact"
+ assert len(self._message) == 0, "Cannot send message with call"
+ (contact, ) = self._contacts.itervalues()
+ number = misc_utils.make_ugly(contact.selectedNumber)
+ le = self._asyncQueue.add_async(self._call)
+ le.start(number)
+
+ def cancel(self):
+ le = self._asyncQueue.add_async(self._cancel)
+ le.start()
+
+ def _get_message(self):
+ return self._message
+
+ def _set_message(self, message):
+ self._message = message
+
+ message = property(_get_message, _set_message)
+
+ def add_contact(self, contactId, messageId, title, description, numbersWithDescriptions):
+ if self._busyReason is not None:
+ raise RuntimeError("Please wait for %r" % self._busyReason)
+ # Allow overwriting of contacts so that the message can be updated and the SMS dialog popped back up
+ contactDetails = _DraftContact(messageId, title, description, numbersWithDescriptions)
+ self._contacts[contactId] = contactDetails
+ self.recipientsChanged.emit()
+
+ def remove_contact(self, contactId):
+ if self._busyReason is not None:
+ raise RuntimeError("Please wait for %r" % self._busyReason)
+ assert contactId in self._contacts, "Contact missing"
+ del self._contacts[contactId]
+ self.recipientsChanged.emit()
+
+ def get_contacts(self):
+ return self._contacts.iterkeys()
+
+ def get_num_contacts(self):
+ return len(self._contacts)
+
+ def get_message_id(self, cid):
+ return self._contacts[cid].messageId
+
+ def get_title(self, cid):
+ return self._contacts[cid].title
+
+ def get_description(self, cid):
+ return self._contacts[cid].description
+
+ def get_numbers(self, cid):
+ return self._contacts[cid].numbers
+
+ def get_selected_number(self, cid):
+ return self._contacts[cid].selectedNumber
+
+ def set_selected_number(self, cid, number):
+ # @note I'm lazy, this isn't firing any kind of signal since only one
+ # controller right now and that is the viewer
+ assert number in (nWD[0] for nWD in self._contacts[cid].numbers), "Number not selectable"
+ self._contacts[cid].selectedNumber = number
+
+ def clear(self):
+ if self._busyReason is not None:
+ raise RuntimeError("Please wait for %r" % self._busyReason)
+ self._clear()
+
+ def _clear(self):
+ oldContacts = self._contacts
+ self._contacts = {}
+ self._message = ""
+ if oldContacts:
+ self.recipientsChanged.emit()
+
+ @contextlib.contextmanager
+ def _busy(self, message):
+ if self._busyReason is not None:
+ raise RuntimeError("Already busy doing %r" % self._busyReason)
+ try:
+ self._busyReason = message
+ yield
+ finally:
+ self._busyReason = None
+
+ def _send(self, numbers, text):
+ self.sendingMessage.emit()
+ try:
+ with self._busy("Sending Text"):
+ with qui_utils.notify_busy(self._errorLog, "Sending Text"):
+ yield (
+ self._backend[0].send_sms,
+ (numbers, text),
+ {},
+ )
+ self.sentMessage.emit()
+ self._clear()
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+
+ def _call(self, number):
+ self.calling.emit()
+ try:
+ with self._busy("Calling"):
+ with qui_utils.notify_busy(self._errorLog, "Calling"):
+ yield (
+ self._backend[0].call,
+ (number, ),
+ {},
+ )
+ self.called.emit()
+ self._clear()
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+
+ def _cancel(self):
+ self.cancelling.emit()
+ try:
+ with qui_utils.notify_busy(self._errorLog, "Cancelling"):
+ yield (
+ self._backend[0].cancel,
+ (),
+ {},
+ )
+ self.cancelled.emit()
+ except Exception, e:
+ _moduleLogger.exception("Reporting error to user")
+ self.error.emit(str(e))
+
+
+class Session(QtCore.QObject):
+
+ # @todo Somehow add support for csv contacts
+ # @BUG When loading without caches, downloads messages twice
+
+ stateChange = qt_compat.Signal(str)
+ loggedOut = qt_compat.Signal()
+ loggedIn = qt_compat.Signal()
+ callbackNumberChanged = qt_compat.Signal(str)
+
+ accountUpdated = qt_compat.Signal()
+ messagesUpdated = qt_compat.Signal()
+ newMessages = qt_compat.Signal()
+ historyUpdated = qt_compat.Signal()
+ dndStateChange = qt_compat.Signal(bool)
+ voicemailAvailable = qt_compat.Signal(str, str)
+
+ error = qt_compat.Signal(str)
+
+ LOGGEDOUT_STATE = "logged out"
+ LOGGINGIN_STATE = "logging in"
+ LOGGEDIN_STATE = "logged in"
+
+ MESSAGE_TEXTS = "Text"
+ MESSAGE_VOICEMAILS = "Voicemail"
+ MESSAGE_ALL = "All"
+
+ HISTORY_RECEIVED = "Received"
+ HISTORY_MISSED = "Missed"
+ HISTORY_PLACED = "Placed"
+ HISTORY_ALL = "All"
+
+ _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.3.0")
+
+ _LOGGEDOUT_TIME = -1
+ _LOGGINGIN_TIME = 0
+
+ def __init__(self, errorLog, cachePath):
+ QtCore.QObject.__init__(self)
+ self._errorLog = errorLog
+ self._pool = qore_utils.FutureThread()
+ self._asyncQueue = concurrent.AsyncTaskQueue(self._pool)
+ self._backend = []
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self._loginOps = []
+ self._cachePath = cachePath
+ self._voicemailCachePath = None
+ self._username = None
+ self._password = None
+ self._draft = Draft(self._asyncQueue, self._backend, self._errorLog)
+ self._delayedRelogin = QtCore.QTimer()
+ self._delayedRelogin.setInterval(0)
+ self._delayedRelogin.setSingleShot(True)
+ self._delayedRelogin.timeout.connect(self._on_delayed_relogin)
+
+ self._contacts = {}
+ self._accountUpdateTime = datetime.datetime(1971, 1, 1)
+ self._messages = []
+ self._cleanMessages = []
+ self._messageUpdateTime = datetime.datetime(1971, 1, 1)
+ self._history = []
+ self._historyUpdateTime = datetime.datetime(1971, 1, 1)
+ self._dnd = False
+ self._callback = ""
+
+ @property
+ def state(self):
+ return {
+ self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
+ self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
+ }.get(self._loggedInTime, self.LOGGEDIN_STATE)
+
+ @property
+ def draft(self):
+ return self._draft
+
+ def login(self, username, password):
+ assert self.state == self.LOGGEDOUT_STATE, "Can only log-in when logged out (currently %s" % self.state
+ assert username != "", "No username specified"
+ if self._cachePath is not None:
+ cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
+ else:
+ cookiePath = None
+
+ if self._username != username or not self._backend:
+ from backends import gv_backend
+ del self._backend[:]
+ self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
+
+ self._pool.start()
+ le = self._asyncQueue.add_async(self._login)
+ le.start(username, password)
+
+ def logout(self):
+ assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
+ _moduleLogger.info("Logging out")
+ self._pool.stop()
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self._backend[0].persist()
+ self._save_to_cache()
+ self._clear_voicemail_cache()
+ self.stateChange.emit(self.LOGGEDOUT_STATE)
+ self.loggedOut.emit()
+
+ def clear(self):
+ assert self.state == self.LOGGEDOUT_STATE, "Can only clear when logged out (currently %s" % self.state
+ self._backend[0].logout()
+ del self._backend[0]
+ self._clear_cache()
+ self._draft.clear()
+
+ def logout_and_clear(self):
+ assert self.state != self.LOGGEDOUT_STATE, "Can only logout if logged in (currently %s" % self.state
+ _moduleLogger.info("Logging out and clearing the account")
+ self._pool.stop()
+ self._loggedInTime = self._LOGGEDOUT_TIME
+ self.clear()
+ self.stateChange.emit(self.LOGGEDOUT_STATE)
+ self.loggedOut.emit()
+
+ def update_account(self, force = True):
+ if not force and self._contacts:
+ return
+ le = self._asyncQueue.add_async(self._update_account), (), {}
+ self._perform_op_while_loggedin(le)
+
+ def refresh_connection(self):
+ le = self._asyncQueue.add_async(self._refresh_authentication)
+ le.start()
+
+ def get_contacts(self):
+ return self._contacts
+
+ def get_when_contacts_updated(self):
+ return self._accountUpdateTime
+
+ def update_messages(self, messageType, force = True):
+ if not force and self._messages:
+ return
+ le = self._asyncQueue.add_async(self._update_messages), (messageType, ), {}
+ self._perform_op_while_loggedin(le)
+
+ def get_messages(self):
+ return self._messages
+
+ def get_when_messages_updated(self):
+ return self._messageUpdateTime
+
+ def update_history(self, historyType, force = True):
+ if not force and self._history:
+ return
+ le = self._asyncQueue.add_async(self._update_history), (historyType, ), {}
+ self._perform_op_while_loggedin(le)
+
+ def get_history(self):
+ return self._history
+
+ def get_when_history_updated(self):
+ return self._historyUpdateTime
+
+ def update_dnd(self):
+ le = self._asyncQueue.add_async(self._update_dnd), (), {}
+ self._perform_op_while_loggedin(le)
+
+ def set_dnd(self, dnd):
+ le = self._asyncQueue.add_async(self._set_dnd)
+ le.start(dnd)
+
+ def is_available(self, messageId):
+ actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+ return os.path.exists(actualPath)
+
+ def voicemail_path(self, messageId):
+ actualPath = os.path.join(self._voicemailCachePath, "%s.mp3" % messageId)
+ if not os.path.exists(actualPath):
+ raise RuntimeError("Voicemail not available")
+