VoiceMac/Classes/VoiceBase/AddressBook/MGMContacts.m

563 lines
21 KiB
Objective-C

//
// MGMContacts.m
// VoiceBase
//
// Created by Mr. Gecko on 8/18/10.
// Copyright (c) 2011 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
//
// Permission to use, copy, modify, and/or distribute this software for any purpose
// with or without fee is hereby granted, provided that the above copyright notice
// and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
// OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
// DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
// ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#import "MGMContacts.h"
#import "MGMContactsProtocol.h"
#import "MGMAddressBook.h"
#import "MGMGoogleContacts.h"
#import "MGMInstance.h"
#import "MGMAddons.h"
#import <MGMUsers/MGMUsers.h>
NSString * const MGMCContactsDB = @"contacts.db";
NSString * const MGMCUpdateDB = @"update.db";
NSString * const MGMCWordSA = @"%@*";
NSString * const MGMCWordSSA = @" %@*";
NSString * const MGMCWordS = @" %@";
NSString * const MGMCQoute = @"\"";
NSString * const MGMCNum = @"%%%@%%";
NSString * const MGMCNEAR = @"NEAR";
NSString * const MGMCAND = @"AND";
NSString * const MGMCOR = @"OR";
NSString * const MGMCNOT = @"NOT";
NSString * const MGMCCNumber = @"%@ (%@)";
NSString * const MGMTiffExt = @"tif";
const int MGMCMaxResults = 10;
@interface MGMContacts (MGMPrivate)
- (void)updated;
@end
@implementation MGMContacts
+ (id)contactsWithClass:(Class)theClass delegate:(id)theDelegate {
return [[[self alloc] initWithClass:theClass delegate:theDelegate] autorelease];
}
- (id)initWithClass:(Class)theClass delegate:(id)theDelegate {
if ((self = [super init])) {
maxResults = MGMCMaxResults;
delegate = theDelegate;
user = [delegate user];
contactsLock = [NSLock new];
isUpdating = NO;
stopingUpdate = NO;
updateLock = [NSLock new];
if (theClass==NULL) {
[self release];
self = nil;
} else {
contacts = [[theClass alloc] initWithDelegate:delegate];
if (contacts==nil) {
[self release];
self = nil;
} else {
if ([[NSFileManager defaultManager] fileExistsAtPath:[[user supportPath] stringByAppendingPathComponent:MGMCContactsDB]]) {
[self setContactsConnection:[MGMLiteConnection connectionWithPath:[[user supportPath] stringByAppendingPathComponent:MGMCContactsDB]]];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userUpdated:) name:MGMUserUpdatedNotification object:nil];
}
}
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[updateLock lock];
[updateLock unlock];
[updateLock release];
[contactsLock lock];
[contactsLock unlock];
[contactsLock release];
[contactsConnection release];
[updateConnection release];
[contacts release];
[super dealloc];
}
- (void)setDelegate:(id)theDelegate {
delegate = theDelegate;
}
- (id<MGMContactsOwnerDelegate>)delegate {
return delegate;
}
- (void)stop {
if (isUpdating) {
stopingUpdate = YES;
[contacts stop];
[updateLock lock];
[updateLock unlock];
[updateConnection release];
updateConnection = nil;
isUpdating = NO;
stopingUpdate = NO;
}
}
- (void)userUpdated:(NSNotification *)theNotification {
MGMUser *theUser = [theNotification object];
if ([theUser isEqual:user] && (![[theUser settingForKey:MGMSContactsSourceKey] isEqual:NSStringFromClass([contacts class])] || ([contacts isKindOfClass:[MGMGoogleContacts class]] && ![[[(MGMGoogleContacts *)contacts user] settingForKey:MGMUserID] isEqual:[theUser settingForKey:MGMCGoogleContactsUser]]))) {
if (stopingUpdate) return;
[self stop];
[contacts release];
contacts = [[NSClassFromString([theUser settingForKey:MGMSContactsSourceKey]) alloc] initWithDelegate:delegate];
[self updateContacts];
}
}
- (void)setMaxResults:(int)theMaxResults {
maxResults = theMaxResults;
}
- (int)maxResults {
return maxResults;
}
- (MGMLiteConnection *)contactsConnection {
return contactsConnection;
}
- (void)setContactsConnection:(MGMLiteConnection *)theConnection {
[contactsLock lock];
[contactsConnection release];
contactsConnection = [theConnection retain];
[contactsLock unlock];
}
- (void)updateContacts {
if (isUpdating)
return;
isUpdating = YES;
[updateLock lock];
NSFileManager *manager = [NSFileManager defaultManager];
if ([manager fileExistsAtPath:[[user supportPath] stringByAppendingPathComponent:MGMCUpdateDB]])
[manager removeItemAtPath:[[user supportPath] stringByAppendingPathComponent:MGMCUpdateDB]];
updateConnection = [[MGMLiteConnection connectionWithPath:[[user supportPath] stringByAppendingPathComponent:MGMCUpdateDB]] retain];
if (updateConnection==nil) {
[self contactsError:nil];
}
#if MGMContactsDebug
NSLog(@"Getting Contacts");
[updateConnection setLogQuery:YES];
#endif
[updateConnection query:@"CREATE VIRTUAL TABLE contacts USING fts3(name, company, number INTEGER, label, photo)"];
[updateConnection query:@"CREATE TABLE groups (docid INTEGER PRIMARY KEY AUTOINCREMENT, name)"];
[updateConnection query:@"CREATE TABLE groupMembers (docid INTEGER PRIMARY KEY AUTOINCREMENT, groupid INTEGER, contactid INTEGER)"];
[updateLock unlock];
[contacts getContacts:self];
}
- (void)gotContact:(NSDictionary *)theContact {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
[updateLock lock];
if (updateConnection!=nil) {
[updateConnection query:@"INSERT INTO contacts (name, company, number, label, photo) VALUES (%@, %@, %@, %@, %@)", [theContact objectForKey:MGMCName], [theContact objectForKey:MGMCCompany], [theContact objectForKey:MGMCNumber], [theContact objectForKey:MGMCLabel], [theContact objectForKey:MGMCPhoto]];
#if MGMContactsDebug
if ([updateConnection errorID]!=0)
NSLog(@"%@ %@", [updateConnection errorMessage], theContact);
#endif
}
[updateLock unlock];
[pool drain];
}
- (void)doneGettingContacts {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
[contacts getGroups:self];
#if MGMContactsDebug
NSLog(@"Finished Adding Contacts");
#endif
[pool drain];
}
- (void)contactsError:(NSError *)theError {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSLog(@"MGMContacts Error: %@", theError);
isUpdating = NO;
[pool drain];
}
- (void)gotGroup:(NSString *)theName withMembers:(NSArray *)theMembers {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
[updateLock lock];
if (updateConnection!=nil) {
[updateConnection query:@"INSERT INTO groups (name) VALUES (%@)", theName];
long long int groupID = [updateConnection insertId];
for (unsigned int i=0; i<[theMembers count]; i++) {
NSDictionary *result = [[updateConnection query:@"SELECT docid, * FROM contacts WHERE number = %@", [theMembers objectAtIndex:i]] nextRow];
if (result!=nil)
[updateConnection query:@"INSERT INTO groupMembers (groupid, contactid) VALUES (%qi, %@)", groupID, [result objectForKey:MGMCDocID]];
}
}
[updateLock unlock];
[pool drain];
}
- (void)doneGettingGroups {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
[self updated];
#if MGMContactsDebug
NSLog(@"Finished Adding Groups");
#endif
[pool drain];
}
- (void)groupsError:(NSError *)theError {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSLog(@"MGMContacts Group Error: %@", theError);
[self updated];
[pool drain];
}
- (void)updated {
[updateLock lock];
[self setContactsConnection:nil];
NSFileManager *manager = [NSFileManager defaultManager];
if ([manager fileExistsAtPath:[[user supportPath] stringByAppendingPathComponent:MGMCContactsDB]])
[manager removeItemAtPath:[[user supportPath] stringByAppendingPathComponent:MGMCContactsDB]];
[manager moveItemAtPath:[[user supportPath] stringByAppendingPathComponent:MGMCUpdateDB] toPath:[[user supportPath] stringByAppendingPathComponent:MGMCContactsDB]];
[self setContactsConnection:[MGMLiteConnection connectionWithPath:[[user supportPath] stringByAppendingPathComponent:MGMCContactsDB]]];
[updateLock unlock];
isUpdating = NO;
if (delegate!=nil && [delegate respondsToSelector:@selector(updatedContacts)]) [delegate updatedContacts];
}
- (NSNumber *)countContactsMatching:(NSString *)theString {
if (contactsConnection==nil)
return [NSNumber numberWithInt:0];
[contactsLock lock];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSString *string = [theString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
MGMLiteResult *result;
if (theString==nil || [string isEqual:@""]) {
result = [contactsConnection query:@"SELECT COUNT(docid) AS count FROM contacts"];
} else {
if ([string isPhone]) {
NSString *search = [NSString stringWithFormat:MGMCNum, [[string removePhoneWhiteSpace] littersToNumbers]];
result = [contactsConnection query:@"SELECT COUNT(docid) AS count FROM contacts WHERE number LIKE %@", search];
} else {
NSArray *words = [string componentsSeparatedByString:@" "];
NSMutableString *search = [NSMutableString string];
BOOL quote = NO;
for (int i=0; i<[words count]; i++) {
NSString *word = [words objectAtIndex:i];
if (quote) {
quote = ![word hasSuffix:MGMCQoute];
if (i==0)
[search appendString:word];
else
[search appendFormat:MGMCWordS, word];
} else {
quote = [word hasPrefix:MGMCQoute];
if (quote) {
i--;
continue;
}
if (i==0)
[search appendFormat:MGMCWordSA, word];
else {
if ([word hasPrefix:MGMCNEAR] || [word isEqual:MGMCAND] || [word isEqual:MGMCOR] || [word isEqual:MGMCNOT]) {
[search appendFormat:MGMCWordS, word];
} else {
[search appendFormat:MGMCWordSSA, word];
}
}
}
}
result = [contactsConnection query:@"SELECT COUNT(docid) AS count FROM contacts WHERE contacts MATCH %@", search];
}
}
NSNumber *count = [[[result nextRow] objectForKey:@"count"] copy];
[contactsLock unlock];
[pool drain];
return [count autorelease];
}
- (NSArray *)contactsMatching:(NSString *)theString page:(int)thePage {
return [self contactsMatching:theString page:thePage includePhoto:YES];
}
- (NSArray *)contactsMatching:(NSString *)theString page:(int)thePage includePhoto:(BOOL)shouldIncludePhoto {
if (contactsConnection==nil)
return nil;
[contactsLock lock];
NSString *select = nil;
if (shouldIncludePhoto)
select = @"SELECT docid, name, company, number, label, photo";
else
select = @"SELECT docid, name, company, number, label";
NSMutableArray *contactsArray = [NSMutableArray array];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSString *string = [theString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
MGMLiteResult *result;
if (thePage==0) thePage = 1;
long long int page = (thePage*maxResults)-maxResults;
if (theString==nil || [string isEqual:@""]) {
result = [contactsConnection query:[select stringByAppendingString:@" FROM contacts ORDER BY company, name LIMIT %qi, %d"], page, maxResults];
} else {
if ([string isPhone]) {
NSString *search = [NSString stringWithFormat:MGMCNum, [[string removePhoneWhiteSpace] littersToNumbers]];
result = [contactsConnection query:[select stringByAppendingString:@" FROM contacts WHERE number LIKE %@ ORDER BY company, name LIMIT %qi, %d"], search, page, maxResults];
} else {
NSArray *words = [string componentsSeparatedByString:@" "];
NSMutableString *search = [NSMutableString string];
BOOL quote = NO;
for (int i=0; i<[words count]; i++) {
NSString *word = [words objectAtIndex:i];
if (quote) {
quote = ![word hasSuffix:MGMCQoute];
if (i==0)
[search appendString:word];
else
[search appendFormat:MGMCWordS, word];
} else {
quote = [word hasPrefix:MGMCQoute];
if (quote) {
i--;
continue;
}
if (i==0)
[search appendFormat:MGMCWordSA, word];
else {
if ([word hasPrefix:MGMCNEAR] || [word isEqual:MGMCAND] || [word isEqual:MGMCOR] || [word isEqual:MGMCNOT]) {
[search appendFormat:MGMCWordS, word];
} else {
[search appendFormat:MGMCWordSSA, word];
}
}
}
}
result = [contactsConnection query:[select stringByAppendingString:@", offsets(contacts) AS offset FROM contacts WHERE contacts MATCH %@ ORDER BY offset LIMIT %qi, %d"], search, page, maxResults];
}
}
NSDictionary *contact = nil;
while ((contact=[result nextRow])!=nil) {
[contactsArray addObject:contact];
}
[contactsLock unlock];
[pool drain];
return contactsArray;
}
- (NSArray *)contactCompletionsMatching:(NSString *)theString {
if (contactsConnection==nil)
return nil;
[contactsLock lock];
NSMutableArray *completions = [NSMutableArray array];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSString *string = [theString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (theString==nil || [string isEqual:@""]) {
[pool drain];
return completions;
} else {
if ([string isPhone]) {
for (int i=0; i<2; i++) {
NSString *search = [NSString stringWithFormat:@"%@%%", (i==0 ? [string phoneFormat] : [string phoneFormatAreaCode:[delegate areaCode]])];
MGMLiteResult *result = [contactsConnection query:@"SELECT name, company, number FROM contacts WHERE number LIKE %@ ORDER BY company, name LIMIT %d", search, maxResults];
NSDictionary *contact = nil;
while ((contact=[result nextRow])!=nil) {
NSString *completion = nil;
NSString *number = [contact objectForKey:MGMCNumber];
if (i==0) {
if (![string hasPrefix:@"+1"] && ![string hasPrefix:@"011"]) {
if ([number hasPrefix:@"+1"])
number = [number substringFromIndex:2];
}
} else {
number = [number substringFromIndex:5];
}
if ([contact objectForKey:MGMCName]!=nil && ![[contact objectForKey:MGMCName] isEqual:@""]) {
completion = [NSString stringWithFormat:MGMCCNumber, number, [contact objectForKey:MGMCName]];
} else if ([contact objectForKey:MGMCCompany]!=nil && ![[contact objectForKey:MGMCCompany] isEqual:@""]) {
completion = [NSString stringWithFormat:MGMCCNumber, number, [contact objectForKey:MGMCCompany]];
} else {
completion = number;
}
[completions addObject:completion];
}
}
} else {
NSArray *words = [string componentsSeparatedByString:@" "];
NSMutableString *search = [NSMutableString string];
for (int i=0; i<[words count]; i++) {
NSString *word = [words objectAtIndex:i];
if (i==0)
[search appendFormat:MGMCWordSA, word];
else {
[search appendFormat:MGMCWordSSA, word];
}
}
MGMLiteResult *result = [contactsConnection query:@"SELECT name, company, label, number, offsets(contacts) AS offset FROM contacts WHERE contacts MATCH %@ ORDER BY offset LIMIT %d", search, maxResults];
NSDictionary *contact = nil;
while ((contact=[result nextRow])!=nil) {
NSString *completion = nil;
if (([contact objectForKey:MGMCName]!=nil && ![[contact objectForKey:MGMCName] isEqual:@""]) || ([contact objectForKey:MGMCCompany]!=nil && ![[contact objectForKey:MGMCCompany] isEqual:@""])) {
NSArray *words = [string componentsSeparatedByString:@" "];
NSString *name = nil;
if ([contact objectForKey:MGMCName]!=nil && ![[contact objectForKey:MGMCName] isEqual:@""])
name = [contact objectForKey:MGMCName];
else
name = [contact objectForKey:MGMCCompany];
if (![name hasPrefix:[words objectAtIndex:0]]) {
NSMutableArray *nameArray = [NSMutableArray arrayWithArray:[name componentsSeparatedByString:@" "]];
for (int i=0; i<[nameArray count]; i++) {
if ([[nameArray objectAtIndex:i] hasPrefix:[words objectAtIndex:0]]) {
name = [nameArray objectAtIndex:i];
[nameArray removeObjectAtIndex:i];
break;
}
}
for (int i=0; i<[nameArray count]; i++) {
name = [name stringByAppendingFormat:MGMCWordS, [nameArray objectAtIndex:i]];
}
}
if ([[contact objectForKey:MGMCLabel] isEqual:@""])
completion = [NSString stringWithFormat:@"%@ <%@>", name, [[contact objectForKey:MGMCNumber] readableNumber]];
else
completion = [NSString stringWithFormat:@"%@ <%@> (%@)", name, [[contact objectForKey:MGMCNumber] readableNumber], [contact objectForKey:MGMCLabel]];
} else {
completion = [[contact objectForKey:MGMCNumber] readableNumber];
}
[completions addObject:completion];
}
}
}
[contactsLock unlock];
[pool drain];
return completions;
}
- (NSDictionary *)contactWithID:(NSNumber *)theID {
if (contactsConnection==nil)
return nil;
return [[contactsConnection query:@"SELECT docid, * FROM contacts WHERE docid = %@", theID] nextRow];
}
- (NSData *)photoDataForNumber:(NSString *)theNumber {
NSDictionary *contact = [[contactsConnection query:@"SELECT photo FROM contacts WHERE number = %@ AND photo NOT NULL", theNumber] nextRow];
if (contact!=nil)
return [contact objectForKey:MGMCPhoto];
return nil;
}
- (NSString *)cachedPhotoForNumber:(NSString *)theNumber {
NSFileManager *manager = [NSFileManager defaultManager];
#if !TARGET_OS_IPHONE
if ([delegate respondsToSelector:@selector(userNumber)] && [[delegate userNumber] isEqual:theNumber]) {
if (![manager fileExistsAtPath:[[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]]]) {
NSData *photo = [MGMAddressBook userPhotoData];
if (photo!=nil) {
[photo writeToFile:[[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]] atomically:YES];
return [[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]];
}
} else {
return [[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]];
}
}
#endif
if (![manager fileExistsAtPath:[[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]]]) {
NSData *photo = [self photoDataForNumber:theNumber];
if (photo!=nil) {
[photo writeToFile:[[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]] atomically:YES];
return [[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]];
}
} else {
return [[MGMUser cachePath] stringByAppendingPathComponent:[theNumber stringByAppendingPathExtension:MGMTiffExt]];
}
return nil;
}
- (NSString *)nameForNumber:(NSString *)theNumber {
NSDictionary *contact = [[contactsConnection query:@"SELECT name, company FROM contacts WHERE number = %@ AND (name != '' OR company != '')", theNumber] nextRow];
if (contact!=nil) {
if (![[contact objectForKey:MGMCName] isEqual:@""])
return [contact objectForKey:MGMCName];
else if (![[contact objectForKey:MGMCCompany] isEqual:@""])
return [contact objectForKey:MGMCCompany];
}
if ([theNumber isPhoneComplete])
return [theNumber readableNumber];
return theNumber;
}
- (NSArray *)groups {
if (contactsConnection==nil)
return nil;
[contactsLock lock];
NSMutableArray *groupsArray = [NSMutableArray array];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
MGMLiteResult *result = [contactsConnection query:@"SELECT * FROM groups ORDER BY name"];
NSDictionary *group = nil;
while ((group=[result nextRow])!=nil) {
[groupsArray addObject:group];
}
[contactsLock unlock];
[pool drain];
return groupsArray;
}
- (NSDictionary *)groupWithID:(NSNumber *)theID {
if (contactsConnection==nil)
return nil;
return [[contactsConnection query:@"SELECT * FROM groups WHERE docid = %@", theID] nextRow];
}
- (NSNumber *)membersCountOfGroup:(NSDictionary *)theGroup {
return [self membersCountOfGroupID:[theGroup objectForKey:MGMCDocID]];
}
- (NSNumber *)membersCountOfGroupID:(NSNumber *)theGroup {
if (contactsConnection==nil)
return nil;
[contactsLock lock];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
MGMLiteResult *result = [contactsConnection query:@"SELECT COUNT(docid) AS count FROM groupMembers WHERE groupid = %@", theGroup];
NSNumber *count = [[[result nextRow] objectForKey:@"count"] copy];
[contactsLock unlock];
[pool drain];
return [count autorelease];
}
- (NSArray *)membersOfGroup:(NSDictionary *)theGroup {
return [self membersOfGroupID:[theGroup objectForKey:MGMCDocID]];
}
- (NSArray *)membersOfGroupID:(NSNumber *)theGroup {
if (contactsConnection==nil)
return nil;
[contactsLock lock];
NSMutableArray *memebersArray = [NSMutableArray array];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
MGMLiteResult *result = [contactsConnection query:@"SELECT * FROM groupMembers WHERE groupid = %@", theGroup];
NSDictionary *member = nil;
while ((member=[result nextRow])!=nil) {
NSDictionary *contact = [self contactWithID:[member objectForKey:MGMCContactID]];
if (contact!=nil)
[memebersArray addObject:contact];
}
[contactsLock unlock];
[pool drain];
return memebersArray;
}
- (NSArray *)groupsOfContact:(NSDictionary *)theContact {
if (contactsConnection==nil)
return nil;
[contactsLock lock];
NSMutableArray *groupsArray = [NSMutableArray array];
NSAutoreleasePool *pool = [NSAutoreleasePool new];
MGMLiteResult *result = [contactsConnection query:@"SELECT * FROM groupMembers WHERE contactid = %@", [theContact objectForKey:MGMCDocID]];
NSDictionary *groupMember = nil;
while ((groupMember=[result nextRow])!=nil) {
NSDictionary *group = [self groupWithID:[groupMember objectForKey:MGMCGroupID]];
if (group!=nil)
[groupsArray addObject:group];
}
[contactsLock unlock];
[pool drain];
return groupsArray;
}
@end