Greymeister.net

The Calendar Strikes Back

So normally, adding a Google Calendar to your iOS device is straightforward, functionality I’ve been using since the first iPod touch I bought. I say normally, because if you’re like me, you also have to deal with a Google Apps account managed by your company that uses federated identity. No apps will properly authenticate with my work account unless they use a browser-based login page, so that it can redirect to the company IDM server.

Also, normally, Google Calendar is excellent for sharing calendar information among different accounts. One of their policies is that they removed the capability for allowing subscriptions to your calendar. I’m not sure why, since it seems that I can make the calendar public anyway.

I could of course browse the calendar with the mobile Chrome or Safari, but it’s not forgiving to use a web calendar on a poor cellular connection. It also means no integration with apps like OmniFocus which is very useful part of my daily workflow. I also did not want to start having to invite my personal account for all of my work events.

The only other option I found was to export my calendar into an ics formatted file, something Google provides within the calendar settings page. I could then put that file somewhere accessible via my phone. However, this process would have to be:

  • Automated because out-of-date calendar information is a useless resource.
  • Accessible because I would be using the calendar on my mobile device.
  • Robust because I don’t want to spend my time working on this system in the future.

I came up with 2 scripts that I could run as scheduled tasks on my Mac at home. The first is a Groovy script that uses the Geb browser automation framework. The second is an Applescript that automates importing the ICS file into an iCloud calendar I have.

DownloadGCalIcs.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@Grapes([
    @Grab("org.codehaus.geb:geb-core:0.7.0"),
    @Grab("org.seleniumhq.selenium:selenium-firefox-driver:2.21.0"),
    @Grab("org.seleniumhq.selenium:selenium-chrome-driver:2.21.0"),
    @Grab("org.seleniumhq.selenium:selenium-support:2.21.0")
])

import geb.Browser
import geb.Configuration
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.Keys

gebConfig = """
/*
    This is the Geb configuration file.

    See: http://www.gebish.org/manual/current/configuration.html
*/

import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.remote.DesiredCapabilities

reportsDir = "page-reports"

driver = {
    driver = new FirefoxDriver()
}

waiting {
    timeout = 60
    retryInterval = 0.5
}

environments {

    // run as “grails -Dgeb.env=chrome test-app”
    // See: http://code.google.com/p/selenium/wiki/ChromeDriver
    chrome {
        driver = { new ChromeDriver() }
    }

    // run as “grails -Dgeb.env=firefox test-app”
    // See: http://code.google.com/p/selenium/wiki/FirefoxDriver
    firefox {
        driver = { new FirefoxDriver() }
    }

}
"""

rawGebConfig = new ConfigSlurper().parse(gebConfig)
configuration = new Configuration(rawGebConfig)

println "Starting ICS download script at ${new Date()}"

download_google_ics_file()


println "ICS download script finished at ${new Date()}"

Browser.drive(configuration){}.quit()

void download_google_ics_file() {
    Browser.drive(configuration) {
        go "http://gmail.com"

        // make sure we actually got to the page
        assert title == "Gmail: Email from Google"
        $("#Email").value('my.email@domain.com')
        def loginLink = $("#signIn")
        loginLink.click()

        // check IDM title
        assert title == 'Crappy IDM (Login)'
        $("#IDToken1").value('myusername')
        $("#IDToken2").value(getPwdText())
        // click login link
        $("#IDToken2") << Keys.ENTER

        // Goto calendar and get ICS export
        go "https://www.google.com/calendar?tab=mc"
        def downloadLink = "https://www.google.com/calendar/exporticalzip"
        // now get the bytes
        def bytes = downloadBytes(downloadLink)
        // process byte input stream through a zip input stream
        def byteInputStream = new ByteArrayInputStream(bytes)
        def zipInputStream = new java.util.zip.ZipInputStream(byteInputStream)
        def zipEntry = zipInputStream.getNextEntry()
        assert zipEntry != null
        // write zip entry to file
        def outputFile = new File('/Users/cerwin/Desktop/work.ics')
        outputFile.withOutputStream { out ->
            out << zipInputStream
        }
    }
}
ImportICSToIcloud.applescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
set add_events to "Add events" -- The title of the "Add Events" dialog on your system.
set file_path to (path to desktop as Unicode text) & "work.ics"
set calendar_name to "Work"
tell application "Finder"
    activate
    open file file_path
end tell
tell application "System Events"
    tell application process "iCal"
        set frontmost to true
        tell window add_events
            repeat until (it exists)
                delay 0.2
            end repeat
            click pop up button 1
            repeat until (menu 1 of pop up button 1 exists)
                delay 0.2
            end repeat
            click menu item calendar_name of menu 1 of pop up button 1
            repeat while (menu 1 of pop up button 1 exists)
                delay 0.2
            end repeat
            click button "OK"
        end tell
        delay 2
        click button "Remove Unsafe Alerts" of front window
    end tell
end tell
tell application "Finder"
    delete file file_path -- moves it to the trash
end tell

Thanks to posts on macscripter.net and a StackOverflow answer for how to do the window interaction with iCal. A couple of caveats for the applescript file, I made sure the calendar’s name I import into only exists under the ICLOUD group so that it doesn’t accidentally import into a local calendar. I also haven’t published the method through which I’ll automate this, because I haven’t decided whether to put it into an Automator workflow or just a simple cron job. For me it’s another example of using what is best suited for the job at hand. Geb is still the most useful browser automation framework I’ve found, and, well, Applescript is really the only way I know to directly interact with Mac applications.