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