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