/android/import-contacts

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