/android/import-contacts

To get this branch, use:
bzr branch http://bzr.ed.am/android/import-contacts
6 by edam
- added GPL header comments to all files
1
/*
2
 * Importer.java
3
 *
4
 * Copyright (C) 2009 Tim Marston <edam@waxworlds.org>
5
 *
6
 * This file is part of the Import Contacts program (hereafter referred
7
 * to as "this program"). For more information, see
8
 * http://www.waxworlds.org/edam/software/android/import-contacts
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
24
package org.waxworlds.edam.importcontacts;
1 by edam
Initial import
25
26
import java.util.HashMap;
27
import java.util.HashSet;
28
import java.util.Iterator;
29
import java.util.Set;
30
import java.util.regex.Matcher;
31
import java.util.regex.Pattern;
32
33
import android.content.ContentUris;
34
import android.content.ContentValues;
35
import android.content.SharedPreferences;
36
import android.database.Cursor;
37
import android.net.Uri;
38
import android.os.Message;
39
import android.provider.Contacts;
40
41
public class Importer extends Thread
42
{
3 by edam
- added "all done" message
43
	public final static int ACTION_ABORT = 1;
44
	public final static int ACTION_ALLDONE = 2;
1 by edam
Initial import
45
46
	public final static int RESPONSE_NEGATIVE = 0;
47
	public final static int RESPONSE_POSITIVE = 1;
48
49
	public final static int RESPONSEEXTRA_NONE = 0;
50
	public final static int RESPONSEEXTRA_ALWAYS = 1;
51
52
	private Doit _doit;
53
	private int _response;
54
	private int _responseExtra;
55
	private HashMap< String, Long > _contacts;
56
	private HashMap< Long, HashSet< String > > _contactNumbers;
57
	private HashMap< Long, HashSet< String > > _contactEmails;
58
	private int _mergeSetting;
59
	private int _lastMergeDecision;
60
	private boolean _abort = false;
3 by edam
- added "all done" message
61
	private boolean _isFinished = false;
1 by edam
Initial import
62
63
	public class ContactData
64
	{
65
		class PhoneData
66
		{
67
			public String _number;
68
			public int _type;
69
			public boolean _isPreferred;
70
71
			public PhoneData( String number, int type, boolean isPreferred ) {
72
				_number = number;
73
				_type = type;
74
				_isPreferred = isPreferred;
75
			}
76
77
			public String getNumber() {
78
				return _number;
79
			}
80
81
			public int getType() {
82
				return _type;
83
			}
84
85
			public boolean isPreferred() {
86
				return _isPreferred;
87
			}
88
		}
89
90
		class EmailData
91
		{
92
			private String _email;
93
			public int _type;
94
			private boolean _isPreferred;
95
96
			public EmailData( String email, int type, boolean isPreferred ) {
97
				_email = email;
98
				_type = type;
99
				_isPreferred = isPreferred;
100
			}
101
102
			public String getAddress() {
103
				return _email;
104
			}
105
106
			public int getType() {
107
				return _type;
108
			}
109
110
			public boolean isPreferred() {
111
				return _isPreferred;
112
			}
113
		}
114
115
		public String _name = null;
116
		public HashMap< String, PhoneData > _phones = null;
117
		public HashMap< String, EmailData > _emails = null;
118
119
		protected void setName( String name )
120
		{
121
			_name = name;
122
		}
123
124
		public String getName()
125
		{
126
			return _name;
127
		}
128
129
		protected void addPhone( String number, int type, boolean isPreferred )
130
		{
131
			if( _phones == null ) _phones = new HashMap< String, PhoneData >();
132
			if( !_phones.containsKey( number ) )
133
				_phones.put( number,
134
						new PhoneData( number, type, isPreferred ) );
135
		}
136
137
		protected void addEmail( String email, int type, boolean isPreferred )
138
		{
139
			if( _emails == null ) _emails = new HashMap< String, EmailData >();
140
			if( !_emails.containsKey( email ) )
141
				_emails.put( email, new EmailData( email, type, isPreferred ) );
142
		}
143
	}
144
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
145
	@SuppressWarnings("serial")
1 by edam
Initial import
146
	protected class AbortImportException extends Exception { };
147
148
	public Importer( Doit doit )
149
	{
150
		_doit = doit;
151
152
		SharedPreferences prefs = getSharedPreferences();
9 by edam
- added scroll view to all layouts
153
		_mergeSetting = prefs.getInt( "merge_setting", Doit.ACTION_PROMPT );
1 by edam
Initial import
154
	}
155
156
	@Override
157
	public void run()
158
	{
159
		try
160
		{
161
			// cache current contact names
162
			buildContactsCache();
163
164
			// do the import
165
			onImport();
166
167
			// done!
3 by edam
- added "all done" message
168
			finish( ACTION_ALLDONE );
1 by edam
Initial import
169
		}
170
		catch( AbortImportException e )
171
		{}
3 by edam
- added "all done" message
172
173
		// flag as finished to prevent interrupts
174
		setIsFinished();
175
	}
176
177
	synchronized private void setIsFinished()
178
	{
179
		_isFinished = true;
1 by edam
Initial import
180
	}
181
182
	protected void onImport() throws AbortImportException
183
	{
184
	}
185
186
	public void wake()
187
	{
188
		wake( 0, RESPONSEEXTRA_NONE );
189
	}
190
191
	public void wake( int response )
192
	{
193
		wake( response, RESPONSEEXTRA_NONE );
194
	}
195
196
	synchronized public void wake( int response, int responseExtra )
197
	{
198
		_response = response;
199
		_responseExtra = responseExtra;
200
		notify();
201
	}
202
3 by edam
- added "all done" message
203
	synchronized public boolean setAbort()
1 by edam
Initial import
204
	{
3 by edam
- added "all done" message
205
		if( !_isFinished && !_abort ) {
206
			_abort = true;
207
			notify();
208
			return true;
209
		}
210
		return false;
1 by edam
Initial import
211
	}
212
213
	protected SharedPreferences getSharedPreferences()
214
	{
215
		return _doit.getSharedPreferences();
216
	}
217
218
	protected void showError( int res ) throws AbortImportException
219
	{
220
		showError( _doit.getText( res ).toString() );
221
	}
222
223
	synchronized protected void showError( String message )
224
			throws AbortImportException
225
	{
226
		checkAbort();
227
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
228
				_doit._handler, Doit.MESSAGE_ERROR, message ) );
1 by edam
Initial import
229
		try {
230
			wait();
231
		}
232
		catch( InterruptedException e ) { }
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
233
3 by edam
- added "all done" message
234
		// no need to check if an abortion happened during the wait, we are
235
		// about to finish anyway!
236
		finish( ACTION_ABORT );
1 by edam
Initial import
237
	}
238
239
	protected void showFatalError( int res ) throws AbortImportException
240
	{
241
		showFatalError( _doit.getText( res ).toString() );
242
	}
243
244
	synchronized protected void showFatalError( String message )
245
			throws AbortImportException
246
	{
247
		checkAbort();
248
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
249
				_doit._handler, Doit.MESSAGE_ERROR, message ) );
1 by edam
Initial import
250
		try {
251
			wait();
252
		}
253
		catch( InterruptedException e ) { }
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
254
3 by edam
- added "all done" message
255
		// no need to check if an abortion happened during the wait, we are
256
		// about to finish anyway!
257
		finish( ACTION_ABORT );
1 by edam
Initial import
258
	}
259
260
	protected boolean showContinue( int res ) throws AbortImportException
261
	{
262
		return showContinue( _doit.getText( res ).toString() );
263
	}
264
265
	synchronized protected boolean showContinue( String message )
266
			throws AbortImportException
267
	{
268
		checkAbort();
269
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
270
				_doit._handler, Doit.MESSAGE_CONTINUEORABORT, message ) );
1 by edam
Initial import
271
		try {
272
			wait();
273
		}
274
		catch( InterruptedException e ) { }
3 by edam
- added "all done" message
275
276
		// check if an abortion happened during the wait
277
		checkAbort();
278
1 by edam
Initial import
279
		return _response == RESPONSE_POSITIVE;
280
	}
281
282
	protected void setProgressMessage( int res ) throws AbortImportException
283
	{
284
		checkAbort();
285
		_doit._handler.sendMessage( Message.obtain( _doit._handler,
3 by edam
- added "all done" message
286
				Doit.MESSAGE_SETPROGRESSMESSAGE, getText( res ) ) );
1 by edam
Initial import
287
	}
288
289
	protected void setProgressMax( int maxProgress )
290
			throws AbortImportException
291
	{
292
		checkAbort();
293
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
294
				_doit._handler, Doit.MESSAGE_SETMAXPROGRESS,
1 by edam
Initial import
295
				new Integer( maxProgress ) ) );
296
	}
297
298
	protected void setTmpProgress( int tmpProgress ) throws AbortImportException
299
	{
300
		checkAbort();
301
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
302
				_doit._handler, Doit.MESSAGE_SETTMPPROGRESS,
1 by edam
Initial import
303
				new Integer( tmpProgress ) ) );
304
	}
305
306
	protected void setProgress( int progress ) throws AbortImportException
307
	{
308
		checkAbort();
309
		_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
310
				_doit._handler, Doit.MESSAGE_SETPROGRESS,
1 by edam
Initial import
311
				new Integer( progress ) ) );
312
	}
313
3 by edam
- added "all done" message
314
	protected void finish( int action ) throws AbortImportException
1 by edam
Initial import
315
	{
3 by edam
- added "all done" message
316
		// update UI to reflect action
317
		int message;
318
		switch( action )
319
		{
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
320
		case ACTION_ALLDONE:	message = Doit.MESSAGE_ALLDONE; break;
3 by edam
- added "all done" message
321
		default:	// fall through
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
322
		case ACTION_ABORT:		message = Doit.MESSAGE_ABORT; break;
3 by edam
- added "all done" message
323
		}
324
		_doit._handler.sendEmptyMessage( message );
1 by edam
Initial import
325
3 by edam
- added "all done" message
326
		// stop
327
		throw new AbortImportException();
1 by edam
Initial import
328
	}
329
330
	protected CharSequence getText( int res )
331
	{
332
		return _doit.getText( res );
333
	}
334
335
	protected boolean isImportRequired( String name )
336
			throws AbortImportException
337
	{
338
		checkAbort();
339
		return isImportRequired( name, _mergeSetting );
340
	}
341
3 by edam
- added "all done" message
342
	synchronized private boolean isImportRequired( String name,
343
			int mergeSetting ) throws AbortImportException
1 by edam
Initial import
344
	{
345
		_lastMergeDecision = mergeSetting;
346
347
		// handle special cases
348
		switch( mergeSetting )
349
		{
9 by edam
- added scroll view to all layouts
350
		case Doit.ACTION_KEEP:
1 by edam
Initial import
351
			// if we keep contacts on duplicate, we better check for one
352
			return !_contacts.containsKey( name );
353
9 by edam
- added scroll view to all layouts
354
		case Doit.ACTION_PROMPT:
1 by edam
Initial import
355
			// if we are prompting on duplicate, we better check for one
356
			if( !_contacts.containsKey( name ) )
357
				return true;
358
359
			// ok, it exists, so do prompt
360
			_doit._handler.sendMessage( Message.obtain(
3 by edam
- added "all done" message
361
					_doit._handler, Doit.MESSAGE_MERGEPROMPT, name ) );
1 by edam
Initial import
362
			try {
363
				wait();
364
			}
365
			catch( InterruptedException e ) { }
366
3 by edam
- added "all done" message
367
			// check if an abortion happened during the wait
368
			checkAbort();
369
1 by edam
Initial import
370
			// if "always" was selected, make choice permenant
371
			if( _responseExtra == RESPONSEEXTRA_ALWAYS )
372
				_mergeSetting = _response;
373
374
			// recurse, with out new merge setting
375
			return isImportRequired( name, _response );
376
		}
377
378
		// for all other cases (either overwriting or merging) we will need the
379
		// imported data
380
		return true;
381
	}
382
383
	protected void skipContact() throws AbortImportException
384
	{
385
		checkAbort();
3 by edam
- added "all done" message
386
		_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTSKIPPED );
1 by edam
Initial import
387
	}
388
389
	protected void importContact( ContactData contact )
390
			throws AbortImportException
391
	{
392
		checkAbort();
393
3 by edam
- added "all done" message
394
//		if( !showContinue( "====[ IMPORTING ]====\n: " + contact._name ) )
395
//			finish( ACTION_ABORT );
1 by edam
Initial import
396
397
		ContentValues values = new ContentValues();
398
		boolean uiInformed = false;
399
400
		// does contact exist already?
401
		Uri contactUri = null;
402
		Long id;
403
		if( ( id = (Long)_contacts.get( contact._name ) ) != null )
404
		{
405
			// should we skip this import altogether?
9 by edam
- added scroll view to all layouts
406
			if( _lastMergeDecision == Doit.ACTION_KEEP ) return;
1 by edam
Initial import
407
408
			// get contact's URI
409
			contactUri = ContentUris.withAppendedId(
410
					Contacts.People.CONTENT_URI, id );
411
412
			// should we destroy the existing contact before importing?
9 by edam
- added scroll view to all layouts
413
			if( _lastMergeDecision == Doit.ACTION_OVERWRITE ) {
1 by edam
Initial import
414
				_doit.getContentResolver().delete( contactUri, null, null );
415
				contactUri = null;
416
417
				// upate the UI
3 by edam
- added "all done" message
418
				_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTOVERWRITTEN );
1 by edam
Initial import
419
				uiInformed = true;
420
421
				// update cache
422
				_contacts.remove( contact._name );
423
			}
424
		}
425
426
		// if we don't have a contact URI it is because the contact never
427
		// existed or because we deleted it
428
		if( contactUri == null )
429
		{
430
			// create a new contact
431
			values.put( Contacts.People.NAME, contact._name );
432
			contactUri = _doit.getContentResolver().insert(
433
					Contacts.People.CONTENT_URI, values );
434
			id = ContentUris.parseId( contactUri );
435
			if( id <= 0 ) return;	// shouldn't happen!
436
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
437
			// try to add them to the "My Contacts" group
438
			try {
439
				Contacts.People.addToMyContactsGroup(
12 by edam
- bugfix: add contacts to the "my contacts" group didn't actually work on a real device. So we're doing it a different way.
440
					_doit.getContentResolver(), id );
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
441
			}
442
			catch( IllegalStateException e ) { }
1 by edam
Initial import
443
444
			// update cache
445
			_contacts.put( contact._name, id );
446
447
			// update UI
448
			if( !uiInformed ) {
3 by edam
- added "all done" message
449
				_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTCREATED );
1 by edam
Initial import
450
				uiInformed = true;
451
			}
452
		}
453
454
		// update UI
455
		if( !uiInformed )
3 by edam
- added "all done" message
456
			_doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTMERGED );
1 by edam
Initial import
457
458
		// import contact parts
459
		if( contact._phones != null )
460
			importContactPhones( contactUri, contact._phones );
461
		if( contact._emails != null )
462
			importContactEmails( contactUri, contact._emails );
463
	}
464
465
	private void importContactPhones( Uri contactUri,
466
			HashMap< String, ContactData.PhoneData > phones )
467
	{
468
		Long contactId = ContentUris.parseId( contactUri );
469
		Uri contactPhonesUri = Uri.withAppendedPath( contactUri,
470
				Contacts.People.Phones.CONTENT_DIRECTORY );
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
471
		Set< String > phonesKeys = phones.keySet();
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
472
473
		// add phone numbers
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
474
		Iterator< String > i = phonesKeys.iterator();
1 by edam
Initial import
475
		while( i.hasNext() ) {
476
			ContactData.PhoneData phone = phones.get( i.next() );
477
478
			// we don't want to add this number if it's crap, or it already
479
			// exists (which would cause a duplicate to be created). We don't
480
			// take in to account the type when checking for duplicates. This is
481
			// intentional: types aren't really very reliable. We assume that
482
			// if the number exists at all, it doesn't need importing. Because
483
			// of this, we also can't update the cache (which we don't need to
484
			// anyway, so it's not a problem).
485
			String number = sanitisePhoneNumber( phone._number );
486
			if( number == null ) continue;
487
			HashSet< String > numbers = _contactNumbers.get( contactId );
488
			if( numbers != null && numbers.contains( number ) ) continue;
489
490
			// add phone number
491
			ContentValues values = new ContentValues();
492
			values.put( Contacts.Phones.TYPE, phone._type );
493
			values.put( Contacts.Phones.NUMBER, phone._number );
494
			if( phone._isPreferred ) values.put( Contacts.Phones.ISPRIMARY, 1 );
495
			_doit.getContentResolver().insert( contactPhonesUri, values );
496
		}
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
497
498
		// now add those phone numbers to the cache to prevent the addition of
499
		// duplicate data from another file
500
		i = phonesKeys.iterator();
501
		while( i.hasNext() ) {
502
			ContactData.PhoneData phone = phones.get( i.next() );
503
504
			String number = sanitisePhoneNumber( phone._number );
505
			if( number != null ) {
506
				HashSet< String > numbers = _contactNumbers.get( contactId );
507
				if( numbers == null ) {
508
					_contactNumbers.put( contactId, new HashSet< String >() );
509
					numbers = _contactNumbers.get( contactId );
510
				}
511
				numbers.add( number );
512
			}
513
		}
1 by edam
Initial import
514
	}
515
516
	private void importContactEmails( Uri contactUri,
517
			HashMap< String, ContactData.EmailData > emails )
518
	{
519
		Long contactId = ContentUris.parseId( contactUri );
520
		Uri contactContactMethodsUri = Uri.withAppendedPath( contactUri,
521
				Contacts.People.ContactMethods.CONTENT_DIRECTORY );
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
522
		Set< String > emailsKeys = emails.keySet();
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
523
524
		// add email addresses
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
525
		Iterator< String > i = emailsKeys.iterator();
1 by edam
Initial import
526
		while( i.hasNext() ) {
527
			ContactData.EmailData email = emails.get( i.next() );
528
529
			// like with phone numbers, we don't want to add this email address
530
			// if it exists already or we would introduce duplicates.
531
			String address = sanitiseEmailAddress( email.getAddress() );
532
			if( address == null ) continue;
533
			HashSet< String > addresses = _contactEmails.get( contactId );
534
			if( addresses != null && addresses.contains( address ) ) continue;
535
536
			// add phone number
537
			ContentValues values = new ContentValues();
538
			values.put( Contacts.ContactMethods.KIND, Contacts.KIND_EMAIL );
539
			values.put( Contacts.ContactMethods.DATA, email.getAddress() );
540
			values.put( Contacts.ContactMethods.TYPE, email.getType() );
541
			if( email.isPreferred() )
542
				values.put( Contacts.ContactMethods.ISPRIMARY, 1 );
543
			_doit.getContentResolver().insert( contactContactMethodsUri,
544
					values );
545
		}
7 by edam
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)
546
547
		// now add those email addresses to the cache to prevent the addition of
548
		// duplicate data from another file
549
		i = emailsKeys.iterator();
550
		while( i.hasNext() ) {
551
			ContactData.EmailData email = emails.get( i.next() );
552
553
			String address = sanitiseEmailAddress( email.getAddress() );
554
			if( address != null ) {
555
				HashSet< String > addresses = _contactEmails.get( contactId );
556
				if( addresses == null ) {
557
					_contactEmails.put( contactId, new HashSet< String >() );
558
					addresses = _contactEmails.get( contactId );
559
				}
560
				addresses.add( address );
561
			}
562
		}
1 by edam
Initial import
563
	}
564
3 by edam
- added "all done" message
565
	synchronized protected void checkAbort() throws AbortImportException
1 by edam
Initial import
566
	{
567
		if( _abort ) {
568
			// stop
569
			throw new AbortImportException();
570
		}
571
	}
572
573
	private void buildContactsCache() throws AbortImportException
574
	{
575
		// update UI
576
		setProgressMessage( R.string.doit_caching );
577
578
		String[] cols;
579
		Cursor cur;
580
581
		// init contacts caches
582
		_contacts = new HashMap< String, Long >();
583
		_contactNumbers = new HashMap< Long, HashSet< String > >();
584
		_contactEmails = new HashMap< Long, HashSet< String > >();
585
586
		// query and store map of contact names to ids
587
		cols = new String[] { Contacts.People._ID, Contacts.People.NAME };
588
		cur = _doit.managedQuery( Contacts.People.CONTENT_URI,
589
				cols, null, null, null);
590
		if( cur.moveToFirst() ) {
591
			int idCol = cur.getColumnIndex( Contacts.People._ID );
592
			int nameCol = cur.getColumnIndex( Contacts.People.NAME );
593
			do {
594
				_contacts.put( cur.getString( nameCol ), cur.getLong( idCol ) );
595
			} while( cur.moveToNext() );
596
		}
597
598
		// query and store map of contact ids to sets of phone numbers
599
		cols = new String[] { Contacts.Phones.PERSON_ID,
600
				Contacts.Phones.NUMBER };
601
		cur = _doit.managedQuery( Contacts.Phones.CONTENT_URI,
602
				cols, null, null, null);
603
		if( cur.moveToFirst() ) {
604
			int personIdCol = cur.getColumnIndex( Contacts.Phones.PERSON_ID );
605
			int numberCol = cur.getColumnIndex( Contacts.Phones.NUMBER );
606
			do {
607
				Long id = cur.getLong( personIdCol );
608
				String number = sanitisePhoneNumber(
609
						cur.getString( numberCol ) );
610
				if( number != null ) {
611
					HashSet< String > numbers = _contactNumbers.get( id );
612
					if( numbers == null ) {
613
						_contactNumbers.put( id, new HashSet< String >() );
614
						numbers = _contactNumbers.get( id );
615
					}
616
					numbers.add( number );
617
				}
618
			} while( cur.moveToNext() );
619
		}
620
621
		// query and store map of contact ids to sets of email addresses
622
		cols = new String[] { Contacts.ContactMethods.PERSON_ID,
623
				Contacts.ContactMethods.DATA };
624
		cur = _doit.managedQuery( Contacts.ContactMethods.CONTENT_URI,
625
				cols, Contacts.ContactMethods.KIND + " = ?",
626
				new String[] { "" + Contacts.KIND_EMAIL }, null );
627
		if( cur.moveToFirst() ) {
628
			int personIdCol = cur.getColumnIndex(
629
					Contacts.ContactMethods.PERSON_ID );
630
			int addressCol = cur.getColumnIndex(
631
					Contacts.ContactMethods.DATA );
632
			do {
633
				Long id = cur.getLong( personIdCol );
634
				String address = sanitiseEmailAddress(
635
						cur.getString( addressCol ) );
636
				if( address != null ) {
637
					HashSet< String > addresses = _contactEmails.get( id );
638
					if( addresses == null ) {
639
						_contactEmails.put( id, new HashSet< String >() );
640
						addresses = _contactEmails.get( id );
641
					}
642
					addresses.add( address );
643
				}
644
			} while( cur.moveToNext() );
645
		}
646
	}
647
648
	private String sanitisePhoneNumber( String number )
649
	{
650
		number = number.replaceAll( "[-\\(\\) ]", "" );
651
		Pattern p = Pattern.compile( "^\\+?[0-9]+" );
652
		Matcher m = p.matcher( number );
653
		if( m.lookingAt() ) return m.group( 0 );
654
		return null;
655
	}
656
657
	private String sanitiseEmailAddress( String address )
658
	{
659
		address = address.trim();
660
		Pattern p = Pattern.compile(
661
				"^[^ @]+@[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?(\\.[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?)+$" );
662
		Matcher m = p.matcher( address );
663
		if( m.matches() ) {
664
			String[] bits = address.split( "@" );
665
			return bits[ 0 ] + "@" + bits[ 1 ].toLowerCase();
666
		}
667
		return null;
668
	}
669
}