Skip to content

Commit

Permalink
Full calendar support (experimental)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Filetti committed Feb 9, 2016
1 parent 23dd669 commit 40cf8be
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 99 deletions.
8 changes: 4 additions & 4 deletions JustUsed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/* Begin PBXBuildFile section */
A914066E1C0348EF00C3C2E0 /* SpotlightDocumentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A914066D1C0348EF00C3C2E0 /* SpotlightDocumentTracker.swift */; };
A91406731C036D5500C3C2E0 /* FirefoxHistoryFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91406721C036D5500C3C2E0 /* FirefoxHistoryFetcher.swift */; };
A92411E71C69FAC900DD6449 /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92411E61C69FAC900DD6449 /* CalendarManager.swift */; };
A92411E71C69FAC900DD6449 /* CalendarTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92411E61C69FAC900DD6449 /* CalendarTracker.swift */; };
A92411EF1C69FB3F00DD6449 /* CalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92411EE1C69FB3F00DD6449 /* CalendarEvent.swift */; };
A92411F11C69FD8F00DD6449 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92411F01C69FD8F00DD6449 /* Person.swift */; };
A92411F31C69FDCB00DD6449 /* SwiftyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92411F21C69FDCB00DD6449 /* SwiftyJSON.swift */; };
Expand Down Expand Up @@ -127,7 +127,7 @@
/* Begin PBXFileReference section */
A914066D1C0348EF00C3C2E0 /* SpotlightDocumentTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpotlightDocumentTracker.swift; sourceTree = "<group>"; };
A91406721C036D5500C3C2E0 /* FirefoxHistoryFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirefoxHistoryFetcher.swift; sourceTree = "<group>"; };
A92411E61C69FAC900DD6449 /* CalendarManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = "<group>"; };
A92411E61C69FAC900DD6449 /* CalendarTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarTracker.swift; sourceTree = "<group>"; };
A92411EE1C69FB3F00DD6449 /* CalendarEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarEvent.swift; sourceTree = "<group>"; };
A92411F01C69FD8F00DD6449 /* Person.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = "<group>"; };
A92411F21C69FDCB00DD6449 /* SwiftyJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyJSON.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -195,7 +195,7 @@
A925744C1BCF8EAA004866B4 /* Model */ = {
isa = PBXGroup;
children = (
A92411E61C69FAC900DD6449 /* CalendarManager.swift */,
A92411E61C69FAC900DD6449 /* CalendarTracker.swift */,
A925745D1BCF906A004866B4 /* AppDelegate.swift */,
A925745E1BCF906A004866B4 /* AppSingleton.swift */,
A925745F1BCF906A004866B4 /* LocationSingleton.swift */,
Expand Down Expand Up @@ -528,7 +528,7 @@
A9F507891BC7BA4B00275C6C /* ViewController.swift in Sources */,
A914066E1C0348EF00C3C2E0 /* SpotlightDocumentTracker.swift in Sources */,
A9F3807B1C199E38009251AB /* GenericBrowserHistory.swift in Sources */,
A92411E71C69FAC900DD6449 /* CalendarManager.swift in Sources */,
A92411E71C69FAC900DD6449 /* CalendarTracker.swift in Sources */,
A92574591BCF8FF9004866B4 /* XCGLogger.swift in Sources */,
A91406731C036D5500C3C2E0 /* FirefoxHistoryFetcher.swift in Sources */,
A92574641BCF906A004866B4 /* LocationSingleton.swift in Sources */,
Expand Down
84 changes: 80 additions & 4 deletions JustUsed/DiMe Data/CalendarEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,43 @@

import Foundation
import EventKit
import Contacts

/// Represents an calendar event, as understood by dime
class CalendarEvent: Event {

private(set) var participants: [Person]?
private(set) var participants: [Person] = [Person]()
private(set) var name: String
private(set) var calendar: String
private(set) var location: Location?
private(set) var locString: String?
private(set) var notes: String?
let id: String

override var hash: Int { get {
var outH = name.hash
outH ^= calendar.hash
if let ls = locString {
outH ^= ls.hashValue
}
if let loc = location {
outH ^= loc.hashValue
}
if let not = notes {
outH ^= not.hashValue
}
for p in participants {
outH ^= p.hash
}
return outH
} }

init(fromEKEvent event: EKEvent) {

self.id = event.eventIdentifier
self.name = event.title
self.calendar = event.calendar.title
self.notes = event.notes
if #available(OSX 10.11, *) {
if let structLoc = event.structuredLocation, clloc = structLoc.geoLocation {
location = Location(fromCLLocation: clloc)
Expand All @@ -49,6 +70,36 @@ class CalendarEvent: Event {

super.init()

if event.hasAttendees, let attendees = event.attendees {
for attendee in attendees {
if let name = attendee.name, part = Person(fromString: name) {
// if possible, fetch more data for this person
if #available(OSX 10.11, *) {
if CNContactStore.authorizationStatusForEntityType(.Contacts) == .Authorized {
do {
let store: CNContactStore = AppSingleton.contactStore as! CNContactStore
let predicate = attendee.contactPredicate
let contacts = try store.unifiedContactsMatchingPredicate(predicate, keysToFetch: [CNContactEmailAddressesKey,CNContactMiddleNameKey, CNContactGivenNameKey, CNContactFamilyNameKey])
// put in data from the first returned contact
if contacts.count >= 1 {
part.firstName = contacts[0].givenName
part.lastName = contacts[0].familyName
let midName = contacts[0].middleName.trimmed()
if midName != "" {
part.middleNames = [midName]
}
part.email = (contacts[0].emailAddresses[0].value as! String)
}
} catch {
AppSingleton.log.error("Error while fetching an individual contact for \(self.name):\n\(error)")
}
}
}
participants.append(part)
}
}
}

setStart(event.startDate)
setEnd(event.endDate)

Expand All @@ -60,12 +111,13 @@ class CalendarEvent: Event {
self.name = json["name"].stringValue
self.calendar = json["calendar"].stringValue
self.locString = json["locString"].string
self.notes = json["notes"].string

if let participants = json["participants"].array {
if participants.count > 0 {
self.participants = [Person]()
for participant in participants {
self.participants!.append(Person(fromJson: participant))
self.participants.append(Person(fromJson: participant))
}
}
}
Expand All @@ -90,7 +142,7 @@ class CalendarEvent: Event {
// update values
retDict["calendar"] = calendar
retDict["name"] = name
if let participants = participants {
if participants.count > 0 {
var partArray = [[String: AnyObject]]()
for participant in participants {
partArray.append(participant.getDict())
Expand All @@ -104,6 +156,9 @@ class CalendarEvent: Event {
if let ls = locString {
retDict["locString"] = ls
}
if let not = notes {
retDict["notes"] = not
}

// required
retDict["appId"] = id
Expand All @@ -113,4 +168,25 @@ class CalendarEvent: Event {

return retDict
}
}

/// Compares two calendar events, which are equal only if all their fields are equal
override func isEqual(object: AnyObject?) -> Bool {
if let otherEvent = object as? CalendarEvent {
if name != otherEvent.name || calendar != otherEvent.calendar || notes != otherEvent.notes {
return false
}
if self.location != otherEvent.location {
return false
}

// using redefined version of == for optional collections
if self.participants == otherEvent.participants {
return true
} else {
return false
}
} else {
return false
}
}
}
1 change: 1 addition & 0 deletions JustUsed/DiMe Data/DiMeData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class DiMeBase: NSObject, Dictionariable {
var theDictionary = [String: AnyObject]()

override init() {
theDictionary["actor"] = "JustUsed"
super.init()
}

Expand Down
53 changes: 50 additions & 3 deletions JustUsed/DiMe Data/HistoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ class HistoryManager: NSObject {
}

/// Send the given dictionary to DiMe (assumed to be in correct form due to the use of public callers of this method)
private func sendToDiMe(dimeData: DiMeBase) {
/// If given, calls the success block if request succeeded.
private func sendToDiMe(dimeData: DiMeBase, successBlock: (Void -> Void)? = nil) {

if dimeAvailable {

Expand Down Expand Up @@ -141,7 +142,15 @@ class HistoryManager: NSObject {
self.dimeConnectState(false)
AppSingleton.log.error("Failure when submitting data to dime:\n\(response.result.error!)")
} else {
// JSON(response.value!) to see what dime replied
if let success = successBlock {
let jres = JSON(response.result.value!)
// check if there is an "error" in the response. If so, log it, otherwise report success
if jres["error"] != nil {
AppSingleton.log.error("DiMe reported error:\n\(jres["error"].stringValue)")
} else {
success()
}
}
}
}

Expand Down Expand Up @@ -175,4 +184,42 @@ extension HistoryManager: RecentDocumentUpdateDelegate, BrowserHistoryUpdateDele

}

/// Protocol implementations for calendar updating
/// Protocol implementations for calendar updating
extension HistoryManager: CalendarHistoryDelegate {

func fetchCalendarEvents(block: [CalendarEvent] -> Void) {

if dimeAvailable {

let server_url: String = NSUserDefaults.standardUserDefaults().valueForKey(JustUsedConstants.prefDiMeServerURL) as! String
let user: String = NSUserDefaults.standardUserDefaults().valueForKey(JustUsedConstants.prefDiMeServerUserName) as! String
let password: String = NSUserDefaults.standardUserDefaults().valueForKey(JustUsedConstants.prefDiMeServerPassword) as! String

let credentialData = "\(user):\(password)".dataUsingEncoding(NSUTF8StringEncoding)!
let base64Credentials = credentialData.base64EncodedStringWithOptions([])

let headers = ["Authorization": "Basic \(base64Credentials)"]

Alamofire.request(.GET, server_url + "/data/events?type=http://www.hiit.fi/ontologies/dime/%23CalendarEvent", headers: headers).responseJSON {
response in
if response.result.isFailure {
AppSingleton.log.error("Failure when retrieving calendar events:\n\(response.result.error!)")
} else {
let eventsPack = JSON(response.result.value!)
var retVal = [CalendarEvent]()
if let events = eventsPack.array {
for ev in events {
retVal.append(CalendarEvent(fromJSON: ev))
}
}
block(retVal)
}
}

}
}

func sendCalendarEvent(newEvent: CalendarEvent, successBlock: Void -> Void) {
sendToDiMe(newEvent, successBlock: successBlock)
}
}
11 changes: 10 additions & 1 deletion JustUsed/DiMe Data/Location.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import Foundation
import CoreLocation

struct Location: Dictionariable, Equatable {
struct Location: Dictionariable, Equatable, Hashable {

let latitude: Double
let longitude: Double
Expand All @@ -36,6 +36,15 @@ struct Location: Dictionariable, Equatable {
let speed: Double?
var descriptionLine: String? // not initialized, can be changed later

var hashValue: Int { get {
var outH = latitude.hashValue
outH ^= longitude.hashValue
if let al = altitude {
outH ^= al.hashValue
}
return outH
} }

init(fromCLLocation loc: CLLocation) {
latitude = loc.coordinate.latitude
longitude = loc.coordinate.longitude
Expand Down
63 changes: 51 additions & 12 deletions JustUsed/DiMe Data/Person.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import Foundation
/// A person is represented by this struct (not a class)
class Person: DiMeBase {

private(set) var firstName: String = "N/A"
private(set) var lastName: String = "N/A"
private(set) var middleNames: [String] = [String]()
private(set) var email: String?
var firstName: String = "N/A"
var lastName: String = "N/A"
var middleNames: [String] = [String]()

var email: String?

/// Returns the name in a string separated by spaces, such as "FistName MiddleName1 MiddleName2 LastName"
override var description: String { get {
Expand All @@ -26,7 +27,20 @@ class Person: DiMeBase {
}
outVal += lastName
return outVal
} }
} }

/// Person's hash is based on the hash of all its fields, xorred together
override var hash: Int { get {
var outH = firstName.hash
outH ^= lastName.hash
for mn in middleNames {
outH ^= mn.hash
}
if let em = email {
outH ^= em.hash
}
return outH
} }

/// Generates a person from a string. If there is a comma in the string, it is assumed that the first name after the comma, otherwise first name is the first non-whitespace separated string, and last name is the last. Middle names are assumed to all come after the first name if there was a comma, between first and last if there is no comma.
/// **Fails (returns nil) if the string could not be parsed.**
Expand Down Expand Up @@ -92,19 +106,44 @@ class Person: DiMeBase {
}

override func getDict() -> [String : AnyObject] {
theDictionary["firstName"] = firstName
theDictionary["lastName"] = lastName
var retDict = theDictionary
retDict["firstName"] = firstName
retDict["lastName"] = lastName
if middleNames.count > 0 {
theDictionary["middleNames"] = middleNames
retDict["middleNames"] = middleNames
}
if let em = self.email {
theDictionary["emailAccount"] = em
retDict["emailAccount"] = em
}

// dime-required
theDictionary["@type"] = "Person"
theDictionary["type"] = "http://www.hiit.fi/ontologies/dime/#Person"
retDict["@type"] = "Person"
retDict["type"] = "http://www.hiit.fi/ontologies/dime/#Person"

return theDictionary
return retDict
}

override func isEqual(object: AnyObject?) -> Bool {
if let otherPerson = object as? Person {
if self.firstName != otherPerson.firstName {
return false
}
if self.lastName != otherPerson.lastName {
return false
}
if self.middleNames != otherPerson.middleNames {
return false
}
if let em1 = self.email, em2 = otherPerson.email {
if em1 != em2 {
return false
}
}

return true
} else {
return false
}
}

}
6 changes: 2 additions & 4 deletions JustUsed/Model/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}()

let calendarTracker = CalendarTracker(calendarDelegate: HistoryManager.sharedManager)

// Data sources to display tables in GUI
let browHistoryDataSource = BrowserTrackerDataSource()
let spoHistoryDataSource = RecentDocDataSource()
Expand Down Expand Up @@ -78,10 +80,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
popover.behavior = NSPopoverBehavior.Transient

// fetch nothing to initialize calendar event monitor
// TODO: currently disabled, enable once we have tag support
// let _ = CalendarManager.sharedInstance.currentEventName

// Prepare browser tracking for each browser
browserManager.addFetcher(SafariHistoryFetcher())
browserManager.addFetcher(FirefoxHistoryFetcher())
Expand Down
Loading

0 comments on commit 40cf8be

Please sign in to comment.