/android/import-contacts

To get this branch, use:
bzr branch http://bzr.ed.am/android/import-contacts

« back to all changes in this revision

Viewing changes to src/org/waxworlds/importcontacts/Importer.java

  • Committer: edam
  • Date: 2009-01-13 06:35:26 UTC
  • Revision ID: edam@waxworlds.org-20090113063526-l9t1s9git4bav60a
- 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!?)

Show diffs side-by-side

added added

removed removed

1
1
/*
2
2
 * Importer.java
3
3
 *
4
 
 * Copyright (C) 2009 to 2012 Tim Marston <tim@ed.am>
 
4
 * Copyright (C) 2009 Tim Marston <edam@waxworlds.org>
5
5
 *
6
6
 * This file is part of the Import Contacts program (hereafter referred
7
7
 * to as "this program"). For more information, see
8
 
 * http://ed.am/dev/android/import-contacts
 
8
 * http://www.waxworlds.org/edam/software/android/import-contacts
9
9
 *
10
10
 * This program is free software: you can redistribute it and/or modify
11
11
 * it under the terms of the GNU General Public License as published by
21
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
22
 */
23
23
 
24
 
package am.ed.importcontacts;
 
24
package org.waxworlds.importcontacts;
25
25
 
26
 
import java.util.Arrays;
27
26
import java.util.HashMap;
28
27
import java.util.HashSet;
29
28
import java.util.Iterator;
30
 
import java.util.Locale;
31
29
import java.util.Set;
32
30
import java.util.regex.Matcher;
33
31
import java.util.regex.Pattern;
34
32
 
35
 
import am.ed.importcontacts.Backend.ContactCreationException;
 
33
import android.content.ContentUris;
 
34
import android.content.ContentValues;
36
35
import android.content.SharedPreferences;
 
36
import android.database.Cursor;
 
37
import android.net.Uri;
37
38
import android.os.Message;
 
39
import android.provider.Contacts;
38
40
 
39
41
public class Importer extends Thread
40
42
{
 
43
        public final static int ACTION_GOBACK = 0;
41
44
        public final static int ACTION_ABORT = 1;
42
45
        public final static int ACTION_ALLDONE = 2;
43
46
 
49
52
 
50
53
        private Doit _doit;
51
54
        private int _response;
52
 
        private int _response_extra;
53
 
        private int _merge_setting;
54
 
        private int _last_merge_decision;
 
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;
55
61
        private boolean _abort = false;
56
 
        private boolean _is_finished = false;
57
 
        private ContactsCache _contacts_cache = null;
58
 
        private Backend _backend = null;
 
62
        private boolean _isFinished = false;
59
63
 
60
 
        /**
61
 
         * Data about a contact
62
 
         */
63
64
        public class ContactData
64
65
        {
65
 
                public final static int TYPE_HOME = 0;
66
 
                public final static int TYPE_WORK = 1;
67
 
                public final static int TYPE_MOBILE = 2;        // only used with phones
68
 
                public final static int TYPE_FAX_HOME = 3;      // only used with phones
69
 
                public final static int TYPE_FAX_WORK = 4;      // only used with phones
70
 
                public final static int TYPE_PAGER = 5;         // only used with phones
71
 
 
72
 
                class TypeDetail
73
 
                {
74
 
                        protected int _type;
75
 
 
76
 
                        public TypeDetail( int type )
77
 
                        {
78
 
                                _type = type;
79
 
                        }
80
 
 
81
 
                        public int getType()
82
 
                        {
83
 
                                return _type;
84
 
                        }
85
 
                }
86
 
 
87
 
                class PreferredDetail extends TypeDetail
88
 
                {
89
 
                        protected boolean _is_preferred;
90
 
 
91
 
                        public PreferredDetail( int type, boolean is_preferred )
92
 
                        {
93
 
                                super( type );
94
 
                                _is_preferred = is_preferred;
95
 
                        }
96
 
 
97
 
                        public boolean isPreferred()
98
 
                        {
99
 
                                return _is_preferred;
100
 
                        }
101
 
                }
102
 
 
103
 
                class ExtraDetail extends PreferredDetail
104
 
                {
105
 
                        protected String _extra;
106
 
 
107
 
                        public ExtraDetail( int type, boolean is_preferred, String extra )
108
 
                        {
109
 
                                super( type, is_preferred );
110
 
 
111
 
                                if( extra != null ) extra = extra.trim();
112
 
                                _extra = extra;
113
 
                        }
114
 
 
115
 
                        public String getExtra()
116
 
                        {
117
 
                                return _extra;
118
 
                        }
119
 
 
120
 
                        public void setExtra( String extra )
121
 
                        {
122
 
                                if( extra != null ) extra = extra.trim();
123
 
                                _extra = extra;
124
 
                        }
125
 
                }
126
 
 
127
 
                @SuppressWarnings("serial")
128
 
                protected class ContactNotIdentifiableException extends Exception
129
 
                {
130
 
                }
131
 
 
132
 
                protected String _name = null;
133
 
                protected String _primary_organisation = null;
134
 
                protected boolean _primary_organisation_is_preferred;
135
 
                protected String _primary_number = null;
136
 
                protected int _primary_number_type;
137
 
                protected boolean _primary_number_is_preferred;
138
 
                protected String _primary_email = null;
139
 
                protected boolean _primary_email_is_preferred;
140
 
                protected HashMap< String, ExtraDetail > _organisations = null;
141
 
                protected HashMap< String, PreferredDetail > _numbers = null;
142
 
                protected HashMap< String, PreferredDetail > _emails = null;
143
 
                protected HashMap< String, TypeDetail > _addresses = null;
144
 
 
145
 
                private ContactsCache.CacheIdentifier _cache_identifier = null;
 
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;
146
119
 
147
120
                protected void setName( String name )
148
121
                {
149
122
                        _name = name;
150
123
                }
151
124
 
152
 
                public boolean hasName()
153
 
                {
154
 
                        return _name != null;
155
 
                }
156
 
 
157
125
                public String getName()
158
126
                {
159
127
                        return _name;
160
128
                }
161
129
 
162
 
                protected void addOrganisation( String organisation, String title,
163
 
                        boolean is_preferred )
164
 
                {
165
 
                        organisation = organisation.trim();
166
 
                        if( organisation.length() <= 0 )
167
 
                        {
168
 
                                // TODO: warn that an imported organisation is being ignored
169
 
                                return;
170
 
                        }
171
 
 
172
 
                        if( title != null ) {
173
 
                                title = title.trim();
174
 
                                if( title.length() <= 0 ) title = null;
175
 
                        }
176
 
 
177
 
                        // add the organisation, as non-preferred (we prefer only one
178
 
                        // organisation in finalise() after they're all imported)
179
 
                        if( _organisations == null )
180
 
                                _organisations = new HashMap< String, ExtraDetail >();
181
 
                        if( !_organisations.containsKey( organisation ) )
182
 
                                _organisations.put( organisation,
183
 
                                        new ExtraDetail( 0, false, title ) );
184
 
 
185
 
                        // if this is the first organisation added, or it's a preferred
186
 
                        // organisation and the current primary organisation isn't, then
187
 
                        // record this as the primary organisation.
188
 
                        if( _primary_organisation == null ||
189
 
                                ( is_preferred && !_primary_organisation_is_preferred ) )
190
 
                        {
191
 
                                _primary_organisation = organisation;
192
 
                                _primary_organisation_is_preferred = is_preferred;
193
 
                        }
194
 
                }
195
 
 
196
 
                public boolean hasOrganisations()
197
 
                {
198
 
                        return _organisations != null && _organisations.size() > 0;
199
 
                }
200
 
 
201
 
                public HashMap< String, ExtraDetail > getOrganisations()
202
 
                {
203
 
                        return _organisations;
204
 
                }
205
 
 
206
 
                public boolean hasPrimaryOrganisation()
207
 
                {
208
 
                        return _primary_organisation != null;
209
 
                }
210
 
 
211
 
                public String getPrimaryOrganisation()
212
 
                {
213
 
                        return _primary_organisation;
214
 
                }
215
 
 
216
 
                protected void addNumber( String number, int type,
217
 
                        boolean is_preferred )
218
 
                {
219
 
                        number = sanitisePhoneNumber( number );
220
 
                        if( number == null )
221
 
                        {
222
 
                                // TODO: warn that an imported phone number is being ignored
223
 
                                return;
224
 
                        }
225
 
 
226
 
                        // add the number, as non-preferred (we prefer only one number
227
 
                        // in finalise() after they're all imported)
228
 
                        if( _numbers == null )
229
 
                                _numbers = new HashMap< String, PreferredDetail >();
230
 
                        if( !_numbers.containsKey( number ) )
231
 
                                _numbers.put( number,
232
 
                                        new PreferredDetail( type, false ) );
233
 
 
234
 
                        final Set< Integer > non_voice_types = new HashSet< Integer >(
235
 
                                Arrays.asList( TYPE_FAX_HOME, TYPE_FAX_WORK, TYPE_PAGER ) );
236
 
 
237
 
                        // if this is the first number added, or it's a preferred number
238
 
                        // and the current primary number isn't, or this number is on equal
239
 
                        // standing with the primary number in terms of preference and it is
240
 
                        // a voice number and the primary number isn't, then record this as
241
 
                        // the primary number.
242
 
                        if( _primary_number == null ||
243
 
                                ( is_preferred && !_primary_number_is_preferred ) ||
244
 
                                ( is_preferred == _primary_number_is_preferred &&
245
 
                                        !non_voice_types.contains( type ) &&
246
 
                                        non_voice_types.contains( _primary_number_type ) ) )
247
 
                        {
248
 
                                _primary_number = number;
249
 
                                _primary_number_type = type;
250
 
                                _primary_number_is_preferred = is_preferred;
251
 
                        }
252
 
                }
253
 
 
254
 
                public boolean hasNumbers()
255
 
                {
256
 
                        return _numbers != null && _numbers.size() > 0;
257
 
                }
258
 
 
259
 
                public HashMap< String, PreferredDetail > getNumbers()
260
 
                {
261
 
                        return _numbers;
262
 
                }
263
 
 
264
 
                public boolean hasPrimaryNumber()
265
 
                {
266
 
                        return _primary_number != null;
267
 
                }
268
 
 
269
 
                public String getPrimaryNumber()
270
 
                {
271
 
                        return _primary_number;
272
 
                }
273
 
 
274
 
                protected void addEmail( String email, int type, boolean is_preferred )
275
 
                {
276
 
 
277
 
                        email = sanitisesEmailAddress( email );
278
 
                        if( email == null )
279
 
                        {
280
 
                                // TODO: warn that an imported email address is being ignored
281
 
                                return;
282
 
                        }
283
 
 
284
 
                        // add the email, as non-preferred (we prefer only one email in
285
 
                        // finalise() after they're all imported)
286
 
                        if( _emails == null )
287
 
                                _emails = new HashMap< String, PreferredDetail >();
 
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 >();
288
141
                        if( !_emails.containsKey( email ) )
289
 
                                _emails.put( email, new PreferredDetail( type, false ) );
290
 
 
291
 
                        // if this is the first email added, or it's a preferred email and
292
 
                        // the current primary organisation isn't, then record this as the
293
 
                        // primary email.
294
 
                        if( _primary_email == null ||
295
 
                                ( is_preferred && !_primary_email_is_preferred ) )
296
 
                        {
297
 
                                _primary_email = email;
298
 
                                _primary_email_is_preferred = is_preferred;
299
 
                        }
300
 
                }
301
 
 
302
 
                public boolean hasEmails()
303
 
                {
304
 
                        return _emails != null && _emails.size() > 0;
305
 
                }
306
 
 
307
 
                public HashMap< String, PreferredDetail > getEmails()
308
 
                {
309
 
                        return _emails;
310
 
                }
311
 
 
312
 
                public boolean hasPrimaryEmail()
313
 
                {
314
 
                        return _primary_email != null;
315
 
                }
316
 
 
317
 
                public String getPrimaryEmail()
318
 
                {
319
 
                        return _primary_email;
320
 
                }
321
 
 
322
 
                protected void addAddress( String address, int type )
323
 
                {
324
 
                        address = address.trim();
325
 
                        if( address.length() <= 0 )
326
 
                        {
327
 
                                // TODO: warn that an imported address is being ignored
328
 
                                return;
329
 
                        }
330
 
 
331
 
                        if( _addresses == null ) _addresses =
332
 
                                new HashMap< String, TypeDetail >();
333
 
                        if( !_addresses.containsKey( address ) )
334
 
                                _addresses.put( address, new TypeDetail( type ) );
335
 
                }
336
 
 
337
 
                public boolean hasAddresses()
338
 
                {
339
 
                        return _addresses != null && _addresses.size() > 0;
340
 
                }
341
 
 
342
 
                public HashMap< String, TypeDetail > getAddresses()
343
 
                {
344
 
                        return _addresses;
345
 
                }
346
 
 
347
 
                protected void finalise()
348
 
                        throws ContactNotIdentifiableException
349
 
                {
350
 
                        // ensure that if there is a primary number, it is preferred so
351
 
                        // that there is always one preferred number. Android will assign
352
 
                        // preference to one anyway so we might as well decide one sensibly.
353
 
                        if( _primary_number != null ) {
354
 
                                PreferredDetail data = _numbers.get( _primary_number );
355
 
                                _numbers.put( _primary_number,
356
 
                                        new PreferredDetail( data.getType(), true ) );
357
 
                        }
358
 
 
359
 
                        // do the same for the primary email
360
 
                        if( _primary_email != null ) {
361
 
                                PreferredDetail data = _emails.get( _primary_email );
362
 
                                _emails.put( _primary_email,
363
 
                                        new PreferredDetail( data.getType(), true ) );
364
 
                        }
365
 
 
366
 
                        // do the same for the primary organisation
367
 
                        if( _primary_organisation != null ) {
368
 
                                ExtraDetail data = _organisations.get( _primary_organisation );
369
 
                                _organisations.put( _primary_organisation,
370
 
                                        new ExtraDetail( 0, true, data.getExtra() ) );
371
 
                        }
372
 
 
373
 
                        // create a cache identifier from this contact data, which can be
374
 
                        // used to look-up an existing contact
375
 
                        _cache_identifier = ContactsCache.createIdentifier( this );
376
 
                        if( _cache_identifier == null )
377
 
                                throw new ContactNotIdentifiableException();
378
 
                }
379
 
 
380
 
                public ContactsCache.CacheIdentifier getCacheIdentifier()
381
 
                {
382
 
                        return _cache_identifier;
383
 
                }
384
 
 
385
 
                private String sanitisePhoneNumber( String number )
386
 
                {
387
 
                        number = number.trim();
388
 
                        Pattern p = Pattern.compile( "^[-\\(\\) \\+0-9#*]+" );
389
 
                        Matcher m = p.matcher( number );
390
 
                        if( m.lookingAt() ) return m.group( 0 );
391
 
                        return null;
392
 
                }
393
 
 
394
 
                private String sanitisesEmailAddress( String email )
395
 
                {
396
 
                        email = email.trim();
397
 
                        Pattern p = Pattern.compile(
398
 
                                "^[^ @]+@[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?(\\.[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?)+$" );
399
 
                        Matcher m = p.matcher( email );
400
 
                        if( m.matches() ) {
401
 
                                String[] bits = email.split( "@" );
402
 
                                return bits[ 0 ] + "@" + bits[ 1 ].toLowerCase( Locale.US );
403
 
                        }
404
 
                        return null;
 
142
                                _emails.put( email, new EmailData( email, type, isPreferred ) );
405
143
                }
406
144
        }
407
145
 
408
 
        @SuppressWarnings("serial")
409
146
        protected class AbortImportException extends Exception { };
410
147
 
411
148
        public Importer( Doit doit )
413
150
                _doit = doit;
414
151
 
415
152
                SharedPreferences prefs = getSharedPreferences();
416
 
                _merge_setting = prefs.getInt( "merge_setting", Doit.ACTION_PROMPT );
 
153
                _mergeSetting = prefs.getInt( "merge_setting", 0 );
417
154
        }
418
155
 
419
156
        @Override
421
158
        {
422
159
                try
423
160
                {
424
 
                        // update UI
425
 
                        setProgressMessage( R.string.doit_caching );
426
 
 
427
 
                        // create the appropriate backend
428
 
                        if( Integer.parseInt( android.os.Build.VERSION.SDK ) >= 5 )
429
 
                                _backend = new ContactsContractBackend( _doit );
430
 
                        else
431
 
                                _backend = new ContactsBackend( _doit );
432
 
 
433
 
                        // create a cache of existing contacts and populate it
434
 
                        _contacts_cache = new ContactsCache();
435
 
                        _backend.populateCache( _contacts_cache );
 
161
                        // cache current contact names
 
162
                        buildContactsCache();
436
163
 
437
164
                        // do the import
438
165
                        onImport();
449
176
 
450
177
        synchronized private void setIsFinished()
451
178
        {
452
 
                _is_finished = true;
 
179
                _isFinished = true;
453
180
        }
454
181
 
455
182
        protected void onImport() throws AbortImportException
466
193
                wake( response, RESPONSEEXTRA_NONE );
467
194
        }
468
195
 
469
 
        synchronized public void wake( int response, int response_extra )
 
196
        synchronized public void wake( int response, int responseExtra )
470
197
        {
471
198
                _response = response;
472
 
                _response_extra = response_extra;
 
199
                _responseExtra = responseExtra;
473
200
                notify();
474
201
        }
475
202
 
476
203
        synchronized public boolean setAbort()
477
204
        {
478
 
                if( !_is_finished && !_abort ) {
 
205
                if( !_isFinished && !_abort ) {
479
206
                        _abort = true;
480
207
                        notify();
481
208
                        return true;
498
225
        {
499
226
                checkAbort();
500
227
                _doit._handler.sendMessage( Message.obtain(
501
 
                        _doit._handler, Doit.MESSAGE_ERROR, message ) );
 
228
                                _doit._handler, Doit.MESSAGE_ERROR, message ) );
502
229
                try {
503
230
                        wait();
504
231
                }
505
232
                catch( InterruptedException e ) { }
506
 
 
507
233
                // no need to check if an abortion happened during the wait, we are
508
234
                // about to finish anyway!
509
235
                finish( ACTION_ABORT );
519
245
        {
520
246
                checkAbort();
521
247
                _doit._handler.sendMessage( Message.obtain(
522
 
                        _doit._handler, Doit.MESSAGE_ERROR, message ) );
 
248
                                _doit._handler, Doit.MESSAGE_ERROR, message ) );
523
249
                try {
524
250
                        wait();
525
251
                }
526
252
                catch( InterruptedException e ) { }
527
 
 
528
253
                // no need to check if an abortion happened during the wait, we are
529
254
                // about to finish anyway!
530
255
                finish( ACTION_ABORT );
540
265
        {
541
266
                checkAbort();
542
267
                _doit._handler.sendMessage( Message.obtain(
543
 
                        _doit._handler, Doit.MESSAGE_CONTINUEORABORT, message ) );
 
268
                                _doit._handler, Doit.MESSAGE_CONTINUEORABORT, message ) );
544
269
                try {
545
270
                        wait();
546
271
                }
556
281
        {
557
282
                checkAbort();
558
283
                _doit._handler.sendMessage( Message.obtain( _doit._handler,
559
 
                        Doit.MESSAGE_SETPROGRESSMESSAGE, getText( res ) ) );
 
284
                                Doit.MESSAGE_SETPROGRESSMESSAGE, getText( res ) ) );
560
285
        }
561
286
 
562
 
        protected void setProgressMax( int max_progress )
 
287
        protected void setProgressMax( int maxProgress )
563
288
                        throws AbortImportException
564
289
        {
565
290
                checkAbort();
566
291
                _doit._handler.sendMessage( Message.obtain(
567
 
                        _doit._handler, Doit.MESSAGE_SETMAXPROGRESS,
568
 
                        Integer.valueOf( max_progress ) ) );
 
292
                                _doit._handler, Doit.MESSAGE_SETMAXPROGRESS,
 
293
                                new Integer( maxProgress ) ) );
569
294
        }
570
295
 
571
 
        protected void setTmpProgress( int tmp_progress )
572
 
                throws AbortImportException
 
296
        protected void setTmpProgress( int tmpProgress ) throws AbortImportException
573
297
        {
574
298
                checkAbort();
575
299
                _doit._handler.sendMessage( Message.obtain(
576
 
                        _doit._handler, Doit.MESSAGE_SETTMPPROGRESS,
577
 
                        Integer.valueOf( tmp_progress ) ) );
 
300
                                _doit._handler, Doit.MESSAGE_SETTMPPROGRESS,
 
301
                                new Integer( tmpProgress ) ) );
578
302
        }
579
303
 
580
304
        protected void setProgress( int progress ) throws AbortImportException
581
305
        {
582
306
                checkAbort();
583
307
                _doit._handler.sendMessage( Message.obtain(
584
 
                        _doit._handler, Doit.MESSAGE_SETPROGRESS,
585
 
                        Integer.valueOf( progress ) ) );
 
308
                                _doit._handler, Doit.MESSAGE_SETPROGRESS,
 
309
                                new Integer( progress ) ) );
586
310
        }
587
311
 
588
312
        protected void finish( int action ) throws AbortImportException
591
315
                int message;
592
316
                switch( action )
593
317
                {
594
 
                case ACTION_ALLDONE:    message = Doit.MESSAGE_ALLDONE; break;
 
318
                case ACTION_GOBACK:             message = Doit.MESSAGE_FINISHED_GOBACK; break;
 
319
                case ACTION_ALLDONE:    message = Doit.MESSAGE_FINISHED_ALLDONE; break;
595
320
                default:        // fall through
596
 
                case ACTION_ABORT:              message = Doit.MESSAGE_ABORT; break;
 
321
                case ACTION_ABORT:              message = Doit.MESSAGE_FINISHED; break;
597
322
                }
598
323
                _doit._handler.sendEmptyMessage( message );
599
324
 
606
331
                return _doit.getText( res );
607
332
        }
608
333
 
609
 
        /**
610
 
         * Should we skip a contact, given whether it exists or not and the current
611
 
         * merge setting?  This routine handles throwing up a prompt, if required.
612
 
         * @param contact_detail the display name of the contact
613
 
         * @param exists true if this contact matches one in the cache
614
 
         * @param merge_setting the merge setting to use
615
 
         * @return true if the contact should be skipped outright
616
 
         * @throws AbortImportException
617
 
         */
618
 
        synchronized private boolean shouldWeSkipContact( String contact_detail,
619
 
                boolean exists, int merge_setting ) throws AbortImportException
620
 
        {
621
 
                _last_merge_decision = merge_setting;
 
334
        protected boolean isImportRequired( String name )
 
335
                        throws AbortImportException
 
336
        {
 
337
                checkAbort();
 
338
                return isImportRequired( name, _mergeSetting );
 
339
        }
 
340
 
 
341
        synchronized private boolean isImportRequired( String name,
 
342
                        int mergeSetting ) throws AbortImportException
 
343
        {
 
344
                _lastMergeDecision = mergeSetting;
622
345
 
623
346
                // handle special cases
624
 
                switch( merge_setting )
 
347
                switch( mergeSetting )
625
348
                {
626
 
                case Doit.ACTION_KEEP:
627
 
                        // if we are skipping on a duplicate, check for one
628
 
                        return exists;
629
 
 
630
 
                case Doit.ACTION_PROMPT:
631
 
                        // if we are prompting on duplicate, then we can say that we won't
632
 
                        // skip if there isn't one
633
 
                        if( !exists ) return false;
634
 
 
635
 
                        // ok, duplicate exists, so do prompt
636
 
                        _doit._handler.sendMessage( Message.obtain( _doit._handler,
637
 
                                Doit.MESSAGE_MERGEPROMPT, contact_detail ) );
 
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(
 
360
                                        _doit._handler, Doit.MESSAGE_MERGEPROMPT, name ) );
638
361
                        try {
639
362
                                wait();
640
363
                        }
643
366
                        // check if an abortion happened during the wait
644
367
                        checkAbort();
645
368
 
646
 
                        // if "always" was selected, make choice permanent
647
 
                        if( _response_extra == RESPONSEEXTRA_ALWAYS )
648
 
                                _merge_setting = _response;
 
369
                        // if "always" was selected, make choice permenant
 
370
                        if( _responseExtra == RESPONSEEXTRA_ALWAYS )
 
371
                                _mergeSetting = _response;
649
372
 
650
 
                        // recurse, with our new merge setting
651
 
                        return shouldWeSkipContact( contact_detail, exists, _response );
 
373
                        // recurse, with out new merge setting
 
374
                        return isImportRequired( name, _response );
652
375
                }
653
376
 
654
 
                // for all other cases (either overwriting or merging) we don't skip
655
 
                return false;
 
377
                // for all other cases (either overwriting or merging) we will need the
 
378
                // imported data
 
379
                return true;
656
380
        }
657
381
 
658
382
        protected void skipContact() throws AbortImportException
659
383
        {
660
384
                checkAbort();
661
 
 
662
 
                // show that we're skipping a new contact
663
385
                _doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTSKIPPED );
664
386
        }
665
387
 
668
390
        {
669
391
                checkAbort();
670
392
 
671
 
                // It is expected that we use contact.getCacheIdentifier() here. The
672
 
                // contact we are passed should have been successfully finalise()d,
673
 
                // which includes generating a valid cache identifier.
674
 
                ContactsCache.CacheIdentifier cache_identifier =
675
 
                        contact.getCacheIdentifier();
676
 
 
677
393
//              if( !showContinue( "====[ IMPORTING ]====\n: " + contact._name ) )
678
394
//                      finish( ACTION_ABORT );
679
395
 
680
 
                // attempt to lookup the id of an existing contact in the cache with
681
 
                // this contact data's cache identifier
682
 
                Long id = (Long)_contacts_cache.lookup( cache_identifier );
 
396
                ContentValues values = new ContentValues();
 
397
                boolean uiInformed = false;
683
398
 
684
 
                // check to see if this contact should be skipped
685
 
                if( shouldWeSkipContact( cache_identifier.getDetail(), id != null,
686
 
                        _merge_setting ) )
 
399
                // does contact exist already?
 
400
                Uri contactUri = null;
 
401
                Long id;
 
402
                if( ( id = (Long)_contacts.get( contact._name ) ) != null )
687
403
                {
688
 
                        // show that we're skipping a contact
689
 
                        _doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTSKIPPED );
690
 
                        return;
 
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
 
417
                                _doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTOVERWRITTEN );
 
418
                                uiInformed = true;
 
419
 
 
420
                                // update cache
 
421
                                _contacts.remove( contact._name );
 
422
                        }
691
423
                }
692
424
 
693
 
                // if a contact exists, and we're overwriting, destroy the existing
694
 
                // contact before importing
695
 
                boolean contact_deleted = false;
696
 
                if( id != null && _last_merge_decision == Doit.ACTION_OVERWRITE )
 
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 )
697
428
                {
698
 
                        contact_deleted = true;
 
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!
699
435
 
700
 
                        // remove from device
701
 
                        _backend.deleteContact( id );
 
436
                        // add them to the "My Contacts" group
 
437
                        Contacts.People.addToGroup(
 
438
                                        _doit.getContentResolver(), id,
 
439
                                        Contacts.Groups.GROUP_MY_CONTACTS );
702
440
 
703
441
                        // update cache
704
 
                        _contacts_cache.removeLookup( cache_identifier );
705
 
                        _contacts_cache.removeAssociatedData( id );
706
 
 
707
 
                        // show that we're overwriting a contact
708
 
                        _doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTOVERWRITTEN );
709
 
 
710
 
                        // discard the contact id
711
 
                        id = null;
712
 
                }
713
 
 
714
 
                try {
715
 
                        // if we don't have a contact id yet (or we did, but we destroyed it
716
 
                        // when we deleted the contact), we'll have to create a new contact
717
 
                        if( id == null )
718
 
                        {
719
 
                                // create a new contact
720
 
                                id = _backend.addContact( contact._name );
721
 
 
722
 
                                // update cache
723
 
                                _contacts_cache.addLookup( cache_identifier, id );
724
 
 
725
 
                                // if we haven't already shown that we're overwriting a contact,
726
 
                                // show that we're creating a new contact
727
 
                                if( !contact_deleted )
728
 
                                        _doit._handler.sendEmptyMessage(
729
 
                                                Doit.MESSAGE_CONTACTCREATED );
 
442
                        _contacts.put( contact._name, id );
 
443
 
 
444
                        // update UI
 
445
                        if( !uiInformed ) {
 
446
                                _doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTCREATED );
 
447
                                uiInformed = true;
730
448
                        }
731
 
                        else
732
 
                                // show that we're merging with an existing contact
733
 
                                _doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTMERGED );
734
 
 
735
 
                        // import contact parts
736
 
                        if( contact.hasNumbers() )
737
 
                                importContactPhones( id, contact.getNumbers() );
738
 
                        if( contact.hasEmails() )
739
 
                                importContactEmails( id, contact.getEmails() );
740
 
                        if( contact.hasAddresses() )
741
 
                                importContactAddresses( id, contact.getAddresses() );
742
 
                        if( contact.hasOrganisations() )
743
 
                                importContactOrganisations( id, contact.getOrganisations() );
744
 
                }
745
 
                catch( Backend.ContactCreationException e )
746
 
                {
747
 
                        showError( R.string.error_unabletoaddcontact );
748
 
                }
 
449
                }
 
450
 
 
451
                // update UI
 
452
                if( !uiInformed )
 
453
                        _doit._handler.sendEmptyMessage( Doit.MESSAGE_CONTACTMERGED );
 
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 );
749
460
        }
750
461
 
751
 
        private void importContactPhones( Long id,
752
 
                HashMap< String, ContactData.PreferredDetail > datas )
753
 
                throws ContactCreationException
 
462
        private void importContactPhones( Uri contactUri,
 
463
                        HashMap< String, ContactData.PhoneData > phones )
754
464
        {
 
465
                Long contactId = ContentUris.parseId( contactUri );
 
466
                Uri contactPhonesUri = Uri.withAppendedPath( contactUri,
 
467
                                Contacts.People.Phones.CONTENT_DIRECTORY );
 
468
                Set phonesKeys = phones.keySet();
 
469
 
755
470
                // add phone numbers
756
 
                Set< String > datas_keys = datas.keySet();
757
 
                Iterator< String > i = datas_keys.iterator();
 
471
                Iterator i = phonesKeys.iterator();
758
472
                while( i.hasNext() ) {
759
 
                        String number = i.next();
760
 
                        ContactData.PreferredDetail data = datas.get( number );
 
473
                        ContactData.PhoneData phone = phones.get( i.next() );
761
474
 
762
475
                        // we don't want to add this number if it's crap, or it already
763
476
                        // exists (which would cause a duplicate to be created). We don't
766
479
                        // if the number exists at all, it doesn't need importing. Because
767
480
                        // of this, we also can't update the cache (which we don't need to
768
481
                        // anyway, so it's not a problem).
769
 
                        if( _contacts_cache.hasAssociatedNumber( id, number ) )
770
 
                                continue;
 
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;
771
486
 
772
487
                        // add phone number
773
 
                        _backend.addContactPhone( id, number, data );
774
 
 
775
 
                        // and add this address to the cache to prevent a addition of
776
 
                        // duplicate date from another file
777
 
                        _contacts_cache.addAssociatedNumber( id, 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
                // now add those phone numbers to the cache to prevent the addition of
 
496
                // duplicate data from another file
 
497
                i = phonesKeys.iterator();
 
498
                while( i.hasNext() ) {
 
499
                        ContactData.PhoneData phone = phones.get( i.next() );
 
500
 
 
501
                        String number = sanitisePhoneNumber( phone._number );
 
502
                        if( number != null ) {
 
503
                                HashSet< String > numbers = _contactNumbers.get( contactId );
 
504
                                if( numbers == null ) {
 
505
                                        _contactNumbers.put( contactId, new HashSet< String >() );
 
506
                                        numbers = _contactNumbers.get( contactId );
 
507
                                }
 
508
                                numbers.add( number );
 
509
                        }
778
510
                }
779
511
        }
780
512
 
781
 
        private void importContactEmails( Long id,
782
 
                HashMap< String, ContactData.PreferredDetail > datas )
783
 
                throws ContactCreationException
 
513
        private void importContactEmails( Uri contactUri,
 
514
                        HashMap< String, ContactData.EmailData > emails )
784
515
        {
 
516
                Long contactId = ContentUris.parseId( contactUri );
 
517
                Uri contactContactMethodsUri = Uri.withAppendedPath( contactUri,
 
518
                                Contacts.People.ContactMethods.CONTENT_DIRECTORY );
 
519
                Set emailsKeys = emails.keySet();
 
520
 
785
521
                // add email addresses
786
 
                Set< String > datas_keys = datas.keySet();
787
 
                Iterator< String > i = datas_keys.iterator();
 
522
                Iterator i = emailsKeys.iterator();
788
523
                while( i.hasNext() ) {
789
 
                        String email = i.next();
790
 
                        ContactData.PreferredDetail data = datas.get( email );
 
524
                        ContactData.EmailData email = emails.get( i.next() );
791
525
 
792
 
                        // we don't want to add this email address if it exists already or
793
 
                        // we would introduce duplicates.
794
 
                        if( _contacts_cache.hasAssociatedEmail( id, email ) )
795
 
                                continue;
 
526
                        // like with phone numbers, we don't want to add this email address
 
527
                        // if it exists already or we would introduce duplicates.
 
528
                        String address = sanitiseEmailAddress( email.getAddress() );
 
529
                        if( address == null ) continue;
 
530
                        HashSet< String > addresses = _contactEmails.get( contactId );
 
531
                        if( addresses != null && addresses.contains( address ) ) continue;
796
532
 
797
533
                        // add phone number
798
 
                        _backend.addContactEmail( id, email, data );
799
 
 
800
 
                        // and add this address to the cache to prevent a addition of
801
 
                        // duplicate date from another file
802
 
                        _contacts_cache.addAssociatedEmail( id, email );
803
 
                }
804
 
        }
805
 
 
806
 
        private void importContactAddresses( Long id,
807
 
                HashMap< String, ContactData.TypeDetail > datas )
808
 
                throws ContactCreationException
809
 
        {
810
 
                // add addresses
811
 
                Set< String > datas_keys = datas.keySet();
812
 
                Iterator< String > i = datas_keys.iterator();
813
 
                while( i.hasNext() ) {
814
 
                        String address = i.next();
815
 
                        ContactData.TypeDetail data = datas.get( address );
816
 
 
817
 
                        // we don't want to add this address if it exists already or we
818
 
                        // would introduce duplicates
819
 
                        if( _contacts_cache.hasAssociatedAddress( id, address ) )
820
 
                                continue;
821
 
 
822
 
                        // add postal address
823
 
                        _backend.addContactAddresses( id, address, data );
824
 
 
825
 
                        // and add this address to the cache to prevent a addition of
826
 
                        // duplicate date from another file
827
 
                        _contacts_cache.addAssociatedAddress( id, address );
828
 
                }
829
 
        }
830
 
 
831
 
        private void importContactOrganisations( Long id,
832
 
                HashMap< String, ContactData.ExtraDetail > datas )
833
 
                throws ContactCreationException
834
 
        {
835
 
                // add addresses
836
 
                Set< String > datas_keys = datas.keySet();
837
 
                Iterator< String > i = datas_keys.iterator();
838
 
                while( i.hasNext() ) {
839
 
                        String organisation = i.next();
840
 
                        ContactData.ExtraDetail data = datas.get( organisation );
841
 
 
842
 
                        // we don't want to add this address if it exists already or we
843
 
                        // would introduce duplicates
844
 
                        if( _contacts_cache.hasAssociatedOrganisation( id, organisation ) )
845
 
                                continue;
846
 
 
847
 
                        // add organisation address
848
 
                        _backend.addContactOrganisation( id, organisation, data );
849
 
 
850
 
                        // and add this address to the cache to prevent a addition of
851
 
                        // duplicate date from another file
852
 
                        _contacts_cache.addAssociatedOrganisation( id, organisation );
 
534
                        ContentValues values = new ContentValues();
 
535
                        values.put( Contacts.ContactMethods.KIND, Contacts.KIND_EMAIL );
 
536
                        values.put( Contacts.ContactMethods.DATA, email.getAddress() );
 
537
                        values.put( Contacts.ContactMethods.TYPE, email.getType() );
 
538
                        if( email.isPreferred() )
 
539
                                values.put( Contacts.ContactMethods.ISPRIMARY, 1 );
 
540
                        _doit.getContentResolver().insert( contactContactMethodsUri,
 
541
                                        values );
 
542
                }
 
543
 
 
544
                // now add those email addresses to the cache to prevent the addition of
 
545
                // duplicate data from another file
 
546
                i = emailsKeys.iterator();
 
547
                while( i.hasNext() ) {
 
548
                        ContactData.EmailData email = emails.get( i.next() );
 
549
 
 
550
                        String address = sanitiseEmailAddress( email.getAddress() );
 
551
                        if( address != null ) {
 
552
                                HashSet< String > addresses = _contactEmails.get( contactId );
 
553
                                if( addresses == null ) {
 
554
                                        _contactEmails.put( contactId, new HashSet< String >() );
 
555
                                        addresses = _contactEmails.get( contactId );
 
556
                                }
 
557
                                addresses.add( address );
 
558
                        }
853
559
                }
854
560
        }
855
561
 
860
566
                        throw new AbortImportException();
861
567
                }
862
568
        }
 
569
 
 
570
        private void buildContactsCache() throws AbortImportException
 
571
        {
 
572
                // update UI
 
573
                setProgressMessage( R.string.doit_caching );
 
574
 
 
575
                String[] cols;
 
576
                Cursor cur;
 
577
 
 
578
                // init contacts caches
 
579
                _contacts = new HashMap< String, Long >();
 
580
                _contactNumbers = new HashMap< Long, HashSet< String > >();
 
581
                _contactEmails = new HashMap< Long, HashSet< String > >();
 
582
 
 
583
                // query and store map of contact names to ids
 
584
                cols = new String[] { Contacts.People._ID, Contacts.People.NAME };
 
585
                cur = _doit.managedQuery( Contacts.People.CONTENT_URI,
 
586
                                cols, null, null, null);
 
587
                if( cur.moveToFirst() ) {
 
588
                        int idCol = cur.getColumnIndex( Contacts.People._ID );
 
589
                        int nameCol = cur.getColumnIndex( Contacts.People.NAME );
 
590
                        do {
 
591
                                _contacts.put( cur.getString( nameCol ), cur.getLong( idCol ) );
 
592
                        } while( cur.moveToNext() );
 
593
                }
 
594
 
 
595
                // query and store map of contact ids to sets of phone numbers
 
596
                cols = new String[] { Contacts.Phones.PERSON_ID,
 
597
                                Contacts.Phones.NUMBER };
 
598
                cur = _doit.managedQuery( Contacts.Phones.CONTENT_URI,
 
599
                                cols, null, null, null);
 
600
                if( cur.moveToFirst() ) {
 
601
                        int personIdCol = cur.getColumnIndex( Contacts.Phones.PERSON_ID );
 
602
                        int numberCol = cur.getColumnIndex( Contacts.Phones.NUMBER );
 
603
                        do {
 
604
                                Long id = cur.getLong( personIdCol );
 
605
                                String number = sanitisePhoneNumber(
 
606
                                                cur.getString( numberCol ) );
 
607
                                if( number != null ) {
 
608
                                        HashSet< String > numbers = _contactNumbers.get( id );
 
609
                                        if( numbers == null ) {
 
610
                                                _contactNumbers.put( id, new HashSet< String >() );
 
611
                                                numbers = _contactNumbers.get( id );
 
612
                                        }
 
613
                                        numbers.add( number );
 
614
                                }
 
615
                        } while( cur.moveToNext() );
 
616
                }
 
617
 
 
618
                // query and store map of contact ids to sets of email addresses
 
619
                cols = new String[] { Contacts.ContactMethods.PERSON_ID,
 
620
                                Contacts.ContactMethods.DATA };
 
621
                cur = _doit.managedQuery( Contacts.ContactMethods.CONTENT_URI,
 
622
                                cols, Contacts.ContactMethods.KIND + " = ?",
 
623
                                new String[] { "" + Contacts.KIND_EMAIL }, null );
 
624
                if( cur.moveToFirst() ) {
 
625
                        int personIdCol = cur.getColumnIndex(
 
626
                                        Contacts.ContactMethods.PERSON_ID );
 
627
                        int addressCol = cur.getColumnIndex(
 
628
                                        Contacts.ContactMethods.DATA );
 
629
                        do {
 
630
                                Long id = cur.getLong( personIdCol );
 
631
                                String address = sanitiseEmailAddress(
 
632
                                                cur.getString( addressCol ) );
 
633
                                if( address != null ) {
 
634
                                        HashSet< String > addresses = _contactEmails.get( id );
 
635
                                        if( addresses == null ) {
 
636
                                                _contactEmails.put( id, new HashSet< String >() );
 
637
                                                addresses = _contactEmails.get( id );
 
638
                                        }
 
639
                                        addresses.add( address );
 
640
                                }
 
641
                        } while( cur.moveToNext() );
 
642
                }
 
643
        }
 
644
 
 
645
        private String sanitisePhoneNumber( String number )
 
646
        {
 
647
                number = number.replaceAll( "[-\\(\\) ]", "" );
 
648
                Pattern p = Pattern.compile( "^\\+?[0-9]+" );
 
649
                Matcher m = p.matcher( number );
 
650
                if( m.lookingAt() ) return m.group( 0 );
 
651
                return null;
 
652
        }
 
653
 
 
654
        private String sanitiseEmailAddress( String address )
 
655
        {
 
656
                address = address.trim();
 
657
                Pattern p = Pattern.compile(
 
658
                                "^[^ @]+@[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?(\\.[a-zA-Z]([-a-zA-Z0-9]*[a-zA-z0-9])?)+$" );
 
659
                Matcher m = p.matcher( address );
 
660
                if( m.matches() ) {
 
661
                        String[] bits = address.split( "@" );
 
662
                        return bits[ 0 ] + "@" + bits[ 1 ].toLowerCase();
 
663
                }
 
664
                return null;
 
665
        }
863
666
}