Una sveglia vocale per Android con Google Calendar e Python

Qualche tempo fa ho acquistato un tablet a basso costo, uno di quelli che, per pochi euro, permette di entrare nel magico mondo di Android. Dopo averci preso confidenza, adesso, tutte le mattine, il dispositivo mi sveglia con una discreta voce sintetizzata fornendomi le condizioni del tempo del posto in cui vivo, facendomi ascoltare un brano scelto a caso fra quelli disponibili sul server di casa ed infine riproducendo il radio giornale registrato da Internet un'ora prima. Non contento, ho deciso di aggiungere un'altra funzionalità a cui tenevo particolarmente che consiste nel ricordarmi gli eventi particolari tipo compleanni, onomastici, (ahimè) pagamenti ed impegni vari. Vediamo, in questo articolo, com'è stato possibile tutto questo.

Cosa si può fare con un piccolo terminale Android a basso costo? Una sveglia vocale, ad esempio, che magari, per esempio, sia in grado di ricordarvi, tra le altre cose, gli appuntamenti del giorno. Ma da dove prendere le informazioni che ci servono per farlo? Come poter gestire gli appuntamenti senza dover scrivere righe e righe di codice? Coloro i quali hanno un account Google hanno già la soluzione a portata di mano. Il programma Python che viene presentato in questo post è in grado di leggere i dati presenti su Google Calendar e di passarli al sintetizzatore vocale di Android. Il tutto corredato con quel minimo di configurazione necessaria per l'autenticazione.

Si tratta, quindi, Un sistema che sia in grado di ricordare i nostri appuntamenti, di modificarli, anche da remoto con un semplice browser, che abbia una buona interfaccia grafica e la possibilità di definire durata ed eventuali ripetizioni degli eventi non è un prodotto che si crea dall'oggi al domani; ci vuole del tempo. Tuttavia, per quelli che come me hanno un account Google, il problema non si pone: c'è Google Calendar!
E' da questa idea che parte il lavoro che vi descriverò in queste righe, un lavoro che mi ha tenuto in casa diversi giorni e che mai avrei pensato potesse essere così complesso. Ne valeva la pena? Giudicherete voi!
Prima di andare avanti, però, sarà meglio che descriva gli strumenti di cui abbiamo bisogno.

Il tablet su cui ho lavorato è un Trust Novitab 7” con  Android 4.0.3. Niente di eccezionale ma funziona.
Bisogna installare:

  1. Sl4a (da qui – per installarlo bisogna abilitare le “origini sconosciute” su Impostazioni → Sicurezza)
  2. PythonForAndroid (da qui – per installarlo bisogna abilitare le “origini sconosciute” su Impostazioni → Sicurezza). In effetti il PythonForAndroid è un installatore. Dopo l'installazione, bisogna lanciarlo e cliccare su “Install”.
  3. Sl4a Script Launcher (dal Play Store). E' necessario per lanciare gli script Python attraverso TaskBomb.
  4. TaskBomb (dal Play Store).Per chi lavora con Linux può essere considerato un valido sostituto di crontab. Permette di lanciare i vostri script all'ora che desiderate.

La procedura è la seguente:

  • Selezionate l'icona “Task”
  • Selezionate il pulsante “Add” (+) in alto a destra
  • Dategli un nome … se credete
  • Selezionate “Data”
  • In fondo all'elenco, selezionate “Select script”
  • Selezionate lo script dalla cartella /mnt/sdcard/sl4a/script (sul vostro dipositivo  è sicuramente diversa)
  • Tornate alla schermata principale e selezionate l'icona “Alarms”.
  • Selezionate il pulsante “Add” (+) in alto a destra
  • Selezionate il Task precedentemente creato
  • Selezionate l'ora in cui far girare il task ed il gioco è fatto

Non sono strettamente necessari ma come si fa senza:

  1. BusyBox (dal Play Store). E' una raccolta abbastanza completa dei comandi Unix più utilizzati. Si parla di telnet, find, grep, awk, sed, wget e molti altri. Chi ne può fare a meno?
  2. SSHDroid (dal Play Store). Stanchi di impartire comandi al vostro cellulare con una tastiera touch? Basta lanciare SSHDroid e da questo momento potete accedere al terminale Android via ssh da qualsiasi PC connesso alla rete locale.

A questo punto entriamo nel vivo della discussione e cerchiamo di capire come sia possibile entrare su Google Calendar con il nostro account. Innanzitutto è necessario registrare il nostro applicativo. In altre parole dobbiamo dichiarare a cosa vogliamo accedere e come.
Per fare questo bisogna seguire questo link. Verrà richiesta la propria username e password, la stessa che utilizziamo per accedere a Gmail. Seguendo quindi il link per creare un nuovo progetto si arriverà a questa schermata:

Per creare un nuovo progetto è necessario cliccare sul menu in alto a destra “API Project” e selezionare la voce “Create…”. Il nome scelto sostituirà la scritta “API Project” nella schermata precedente. Poiché vogliamo utilizzare le API del calendario selezioniamo la “slide” a fianco di “Calendar API” e accettiamo i termini del servizio. Adesso, in corrispondenza dell'API che ci interessa vedremo la scritta “ON” su sfondo verde.

Selezioniamo quindi la voce “API Access” dalla lista in alto a sinistra. Sarà subito visibile la “API KEY”, come indicato nella figura seguente, ed un pulsante blu che ci invita a creare “OAuth 2.0 client ID”. Facciamolo.

La schermata successiva ci chiederà il nome del prodotto (unico campo obbligatorio), quella dopo ancora che tipo di accesso ci interessa. Utilizzeremo il default: “Web application”.
Un altro parametro importante è quello indicato con “Your site or hostname”. Probabilmente dovremo ancora modificarlo in seguito ma per adesso scriviamo “localhost” come suggerito.

 

“Client ID” ed “API key” sono due dati molto importanti che dovremo conservare con cura ed utilizzare all'interno dello script python che trovate in fondo a questo articolo.

E' arrivato il momento di parlare di configurazione. Lo script cui prima accennavo necessita di 6 parametri per funzionare e sono quelli seguito elencati.

#User and password                                          
user="XXXXXX@gmail.com"                                 
password="XXXXXX"                                       
                                                            
#Ids from Google API access                                 
clientId="XXXXXXXXXXX.apps.googleusercontent.com"           
myKey="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"              
                                                            
#Port and address configuration to receive the oauth token  
PORT_NUMBER=XXXX                                            
redirectUri="http://localhost:"+str(PORT_NUMBER)"

I primi due sono semplicemente l'utente e la password di Gmail, niente di particolare quindi. I secondi due, sono il Client ID e l'API key che ci siamo ricavati seguendo le istruzioni precedenti. Gli ultimi due … potrebbero creare dei problemi.
Il principio di funzionamento di questa autenticazione si basa sul fatto che, fornite tutte le informazioni necessarie, il server remoto, Google, ci invierà il token per accedere al servizio richiesto attraverso un altro servizio che saremo noi a dover creare e che, tipicamente, dovrà girare sulla macchina da cui facciamo partire lo script.
E' tutto spiegato nel grafico seguente.

Attivare un servizio web sul proprio host con lo scopo di ricevere il token non è un grosso problema. La complicazione sta nel fatto che, se l'host si trova dietro ad un router, in qualche modo bisogna istruire il router stesso per far passare l'informazione.
Vediamo i due casi.

  1. L'host è direttamente connesso ad Internet; è identificato, cioè, da un IP pubblico raggiungibile dall'esterno. I campi “Redirect URIs” e “JavaScript origins” dovranno essere modificati rispettivamente come segue: “http://localhost:9999” e “http://localhost”. In effetti basta modificare solo il primo, però … non si sa mai. Se vi siete persi e non trovate più la pagina giusta, vi ricordo che, per modificare i suddetti parametri, dovete seguire il link https://code.google.com/apis/console, quindi cliccare su “API Console” in alto a sinistra (dopo esservi registrati se necessario) ed infine andare su “Edit settings…” in corrispondenza di “Client ID for web applications”. In questo modo diciamo a Google che siamo in ascolto sulla porta 9999 sulla stessa macchina (localhost) dalla quale abbiamo effettuato la richiesta.
  2. L'host si trova su una rete interna ed è connesso ad Internet tramite un router. Tutti i router di fascia medio alta hanno ormai una funzionalità che li rende in grado di ridirigere una o più porte verso uno degli host della rete interna.

La situazione è quella indicata nella figura precedente. Un host (quello su cui gira lo script) con indirizzo e.f.g.h è collegato ad un router con indirizzo ip a.b.c.d il quale a sua volta è collegato ad Internet. Riutilizzando la porta 9999 del caso precedente, il router andrà configurato in modo tale che tutte le richieste fatte all'indirizzo a.b.c.d (pubblicato su Internet) sulla porta 9999, siano inoltrate all'host e.f.g.h. In questo caso  “Redirect URIs” e “JavaScript origins” dovranno essere impostati rispettivamente a “http://a.b.c.d:9999” e “http://a.b.c.d”.
Importante. I parametri PORT_NUMBER e redirectUri sullo script andranno sempre modificati di conseguenza. In quest'ultimo caso avremo ad esempio:

PORT_NUMBER=9999
redirectUri="http://a.b.c.d:"+str(PORT_NUMBER)"

Siamo ormai pronti per fare la prima prova. Lanciate lo script utilizzando l'interprete python o attraverso l'interfaccia grafica di sl4a (in quest'ultimo caso eseguite lo script su un terminale). Se tutto funziona a dovere, dovrebbe apparire sullo schermo un indirizzo piuttosto lungo che inizia con

https://accounts.google.com/ServiceLoginAuth?timeStmp=&PersistentCookie=yes&service=grandcentral&shdf=…….

Lanciate su un terminale del vostro Android il comando “nc -l -p 9999” (se 9999 è la porta che avete scelto in precedenza. Il comando nc non funzionerà se non avete installato busybox). Copiate ed incollate il precedente URL sul vostro browser e date invio. Il server di Google vi restituirà una pagina sulla quale dovrete autenticarvi e sul terminale da dove avete lanciato il comando nc apparirà qualcosa del tipo

GET / HTTP/1.1
Host: your.ip.address:9999
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:13.0) Gecko/20100101 Firefox/13.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive

Va bene?
Adesso terminate il processo nc – la pagina web che il browser sta tentando di caricare darà un errore – e quindi rilanciatelo esattamente come prima: “nc -l -p 9999”.
A questo punto attivate ancora lo script e sul terminale dove sta girando nc dovrebbe apparire qualcosa di simile:

GET /#state=ok&access_token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&token_type=Bearer&expires_in=3600 HTTP/1.1
Accept-Encoding: identity
Host: your.ip.address:9999
Connection: close
User-Agent: Python-urllib/2.6

Se vi è stato fornito l'access_token, ha funzionato tutto!
E' arrivato il momento di apportare le ultime semplici modifiche allo script. Cercate le parole “START TEST” all'interno del codice. Le troverete due volte. Commentate o cancellate le due righe che troverete sotto la prima occorrenza decommentate le due righe che si trovano sotto la seconda.
Lo script adesso è pronto per funzionare. Lanciatelo di nuovo dopo aver aggiunto un evento al vostro calendario per la data odierna.
Dopo qualche secondo quello che avete scritto per identificare l'evento dovrebbe apparire sul video. Se c'è un evento presente su un'altra data, diciamo il 23/11/2016, lanciatelo come segue:

python <nome che avete dato allo script> 2016-11-23

Anche in questo caso, dopo qualche secondo, dovreste ottenere la descrizione dell'evento.
Ricordate che, per far funzionare lo script, tutte le stringhe che iniziano con “YOUR_” devono essere modificate opportunamente.

Adesso una breve descrizione di come funziona il tutto.
Subito dopo le impostazioni iniziali della funzione main, lo script, in un thread separato, farà partire un http server che si metterà in ascolto sulla porta YOUR_PORT. E' su questa porta che ci arriverà l'informazione per completare tutto il processo.
La successiva requestToken, dentro il try, invierà al server di Google la richiesta del token. Il server risponderà con una pagina per effettuare il login. Per rendere questa procedura automatica, la funzione logInto analizzerà la suddetta pagina con un parser Xml, riempirà i campi login e password e la rispedirà al server. A questo punto, completata l'autenticazione, sulla porta YOUR_PORT sarà inviato l'access_token nel formato evidenziato precedentemente (vedi output del secondo comando nc).
Ottenuto il token (metodo do_GET della classe myHandler), lo script richiederà infine l'evento di interesse con la funzione getEvent, fornendo come parametri il token stesso ed il giorno.

Ecco tre ottimi link per chi vuole sapere di più.
https://developers.google.com/google-apps/calendar/firstapp
https://developers.google.com/google-apps/calendar/concepts
https://developers.google.com/google-apps/calendar/v3/reference/calendars/get

Concludendo.
Ne valeva la pena? Forse no, ma alla fine è stato divertente.

Se questo articolo vi è piaciuto, visitate la questa pagina dove troverete la documentazione del mio ultimo lavoro: Ninobot!

Nino Miano

 

Listato del codice

"""Display the events of a day stored in the Google calendar"""

__author__ = 'Nino Miano <ninomiano66(at)google.com>'
__copyright__ = 'Copyright (c) 2012, Ninux Inc.'
__license__ = 'Ninux License, Version 1.0'

import android
import urllib
import urllib2
import threading
import time
import datetime
import sys
import os

from HTMLParser import HTMLParser
from cookielib import CookieJar, DefaultCookiePolicy
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
from datetime import date

#User and password
user="YOUR_GMAIL_USERNAME@gmail.com"
password="YOUR_PASSWORD"

#Ids from Google API access
clientId="YOUR_CLIENT_ID"
myKey="YOUR_KEY"

#Port and address configuration to receive the oauth token
PORT_NUMBER=YOUR_PORT
redirectUri="http://YOUR_IP:"+str(PORT_NUMBER)

#Addresses to connect to
scopeCalendar="https://www.google.com/calendar/feeds"
oauthAddress="https://accounts.google.com/o/oauth2/auth"
calendarAddress="https://www.googleapis.com/calendar/v3/calendars/"+user+"/events"

#Thread control
canExit=False

#Time range
timeMax=""
timeMin=""

# Opener to read web pages (it must be the same for every call
# to manage properly cookies)
cj = CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))

# Htlm parser to prepare the first authorization request
# We have to create a subclass and override the handler methods
class MyHTMLParser(HTMLParser):
  addressParam={}
  addressVale=""
  skey=""
  svalue=""
  def handle_starttag(self, tag, attrs):
    #Parse every input in the form and store them in addressParam
    if tag == "form":
      for key, value in attrs:
        if key == "action":
          self.addressValue = value
    if tag == "input":
      for key, value in attrs:
        if key == "name":
          self.skey = value
        if key == "value":
          self.svalue = value
      if self.skey != "_utf8": #To avoid utf8conversion errors
        self.addressParam[self.skey] = self.svalue
  def getAddressParam(self):
    return self.addressParam
  def getAddressValue(self):
    return self.addressValue   

#Handle the resquest from Google containing the accesToken
class myHandler(BaseHTTPRequestHandler):
  def do_GET(self):
    key="access_token="
    p1=self.path.find(key)+len(key);
    p2=self.path.index('&',p1);
    accessToken=self.path[p1:p2]
    self.send_response(200)
    self.send_header('Content-type','text/html')
    self.end_headers()
    #Send the request for calendar events
    getEvent(calendarAddress, accessToken, myKey, timeMax, timeMin)
  #Silent server (no log)
  def log_message(self, format, *args):
    return
     
#Httpd server in a thread (it will be run in a thread)
def runHttpd():
  server = HTTPServer(('', PORT_NUMBER), myHandler)
  server.serve_forever()
                                                
#Token request (first request before authentication)
def requestToken(address, client_id, redirect_uri, scope):
  # Data preparing and connection
  data = urllib.urlencode({"client_id" : client_id, "redirect_uri" : redirect_uri,
 "scope" : scope, "response_type" : "token", "state" : "ok", "approval_prompt" : "auto"})
  f = opener.open(address, data)
  page = f.read()
  f.close()
  return page
 
#Log into page after the first request
#It adds Email, Passwd and service parameters
def logInto(authPage, user, password):
  parser = MyHTMLParser()
  parser.feed(authPage)
  addressParam = parser.getAddressParam()
  addressParam["Email"]=user
  addressParam["Passwd"]=password
  addressParam["service"]="grandcentral"
  uAddressParam = urllib.urlencode(addressParam)
  #START TEST (Remove the following two lines after the test)
  print parser.getAddressValue()+"?"+uAddressParam
  sys.exit()
  #END TEST
  f = opener.open(parser.getAddressValue(), uAddressParam)
  page = f.read()
  f.close()
 
#Final step. It gests the events from Google calendar in the range
#tMin - tMax. Authorization sent by header
def getEvent(calendarPage, accessToken, key, tMax, tMin):
  data = urllib.urlencode({"timeMax" : tMax, "timeMin" : tMin, "key" : key})
  req = urllib2.Request(calendarPage+"?"+data)
  req.add_header('Authorization', ' Bearer '+accessToken)
  f = opener.open(req)
  while 1:
    next = f.readline()
    if not next:
      break
    if "summary" in next and user not in next:
      ar=next.split('"')
      print ar[3]
  f.close()
  opener.close()
  global canExit
  canExit=True
   
#Main
#Time range define
#Start httpd thread
#Manage exit strategy
def main():
  #Check parameters
  if len(sys.argv) != 2:
    today = date.today()
  else:
   today = datetime.datetime.strptime(sys.argv[1]+"T00:00:00.000Z", "%Y-%m-%dT%H:%M:%S.000Z")
 
  one_day = datetime.timedelta(days=1)
  tomorrow = today + one_day

  #Range calculation
  global timeMin
  timeMin=today.strftime("%Y-%m-%dT%H:%M:%S.000Z")
  global timeMax
  timeMax=tomorrow.strftime("%Y-%m-%dT%H:%M:%S.000Z")
 
  #Start thread
  #START TEST (uncomment the following two lines after the test)
  #t = threading.Thread(target=runHttpd)
  #t.start()
  #END TEST

  try:
    #Request and login
    authPage=requestToken(oauthAddress, clientId, redirectUri, scopeCalendar)
    logInto(authPage, user, password)
  except:
    print "Errore di connessione"

  #Manage exit
  while 1:
    time.sleep(1)
    if canExit:
      break;
      
  sys.exit()
 
#MAIN
main()

3 Comments

  1. veseo 22 agosto 2013
  2. Nino Miano 26 agosto 2013
  3. veseo 26 agosto 2013

Leave a Reply