/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/VCFImporter.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

21
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
22
 */
23
23
 
24
 
package org.waxworlds.edam.importcontacts;
 
24
package org.waxworlds.importcontacts;
25
25
 
26
26
import java.io.BufferedReader;
27
27
import java.io.File;
38
38
import java.util.regex.Matcher;
39
39
import java.util.regex.Pattern;
40
40
 
 
41
import org.waxworlds.importcontacts.Importer.AbortImportException;
 
42
 
41
43
import android.content.SharedPreferences;
42
44
import android.provider.Contacts;
43
45
import android.provider.Contacts.PhonesColumns;
65
67
                try
66
68
                {
67
69
                        // open directory
68
 
                        String path = "/sdcard" + prefs.getString( "location", "/" );
69
 
                        File file = new File( path );
70
 
                        if( !file.exists() )
 
70
                        String location = prefs.getString( "location", "" );
 
71
                        File dir = new File( location );
 
72
                        if( !dir.exists() || !dir.isDirectory() )
71
73
                                showError( R.string.error_locationnotfound );
72
74
 
73
 
                        // directory, or file?
74
 
                        if( file.isDirectory() )
75
 
                        {
76
 
                                // get files
77
 
                                class VCardFilter implements FilenameFilter {
78
 
                                        public boolean accept( File dir, String name ) {
79
 
                                                return name.toLowerCase().endsWith( ".vcf" );
80
 
                                        }
81
 
                                }
82
 
                                files = file.listFiles( new VCardFilter() );
83
 
                        }
84
 
                        else
85
 
                        {
86
 
                                // use just this file
87
 
                                files = new File[ 1 ];
88
 
                                files[ 0 ] = file;
89
 
                        }
 
75
                        // get files
 
76
                        class VCardFilter implements FilenameFilter {
 
77
                            public boolean accept( File dir, String name ) {
 
78
                                return name.toLowerCase().endsWith( ".vcf" );
 
79
                            }
 
80
                        }
 
81
                        files = dir.listFiles( new VCardFilter() );
90
82
                }
91
83
                catch( SecurityException e ) {
92
84
                        showError( R.string.error_locationpermissions );
138
130
 
139
131
                }
140
132
                catch( FileNotFoundException e ) {
141
 
                        showError( getText( R.string.error_filenotfound ) +
142
 
                                file.getName() );
 
133
                        showError( getText( R.string.error_filenotfound ) + file.getName() );
143
134
                }
144
135
                catch( IOException e ) {
145
136
                        showError( getText( R.string.error_ioerror ) + file.getName() );
163
154
                        importVCardFileContent( content.toString(), file.getName() );
164
155
                }
165
156
                catch( FileNotFoundException e ) {
166
 
                        showError( getText( R.string.error_filenotfound ) +
167
 
                                file.getName() );
 
157
                        showError( getText( R.string.error_filenotfound ) + file.getName() );
168
158
                }
169
159
                catch( IOException e ) {
170
160
                        showError( getText( R.string.error_ioerror ) + file.getName() );
174
164
        private void importVCardFileContent( String content, String fileName )
175
165
                        throws AbortImportException
176
166
        {
 
167
                // unfold RFC2425 section 5.8.1 folded lines, except that we must also
 
168
                // handle embedded Quoted-Printable encodings that have a trailing '='.
 
169
                // So we remove these first before doing RFC2425 unfolding.
 
170
                content = content.replaceAll( "=\n[ \\t]", "" )
 
171
                                .replaceAll( "\n[ \\t]", "" );
 
172
 
177
173
                // get lines and parse them
178
174
                String[] lines = content.split( "\n" );
179
175
                VCard vCard = null;
248
244
 
249
245
                private String _version = null;
250
246
                private Vector< String > _lines = null;
251
 
                private int _name_level = NAMELEVEL_NONE;
252
 
                private boolean _parser_in_multiline = false;
253
 
                private String _parser_current_name_and_params = null;
254
 
                private String _parser_buffered_value_so_far = "";
255
 
 
256
 
                protected class UnencodeResult
257
 
                {
258
 
                        private boolean _another_line_required;
259
 
                        private byte[] _bytes;
260
 
                        private int _num_bytes;
261
 
 
262
 
                        public UnencodeResult( boolean another_line_required, byte[] bytes,
263
 
                                int num_bytes )
264
 
                        {
265
 
                                _another_line_required = another_line_required;
266
 
                                _bytes = bytes;
267
 
                                _num_bytes = num_bytes;
268
 
                        }
269
 
 
270
 
                        public boolean isAnotherLineRequired()
271
 
                        {
272
 
                                return _another_line_required;
273
 
                        }
274
 
 
275
 
                        public byte[] getBytes()
276
 
                        {
277
 
                                return _bytes;
278
 
                        }
279
 
 
280
 
                        public int getNumBytes()
281
 
                        {
282
 
                                return _num_bytes;
283
 
                        }
284
 
                }
285
 
 
286
 
                @SuppressWarnings("serial")
 
247
                private int _nameLevel = NAMELEVEL_NONE;
 
248
 
287
249
                protected class ParseException extends Exception
288
250
                {
289
 
                        @SuppressWarnings("unused")
290
251
                        public ParseException( String error )
291
252
                        {
292
253
                                super( error );
298
259
                        }
299
260
                }
300
261
 
301
 
                @SuppressWarnings("serial")
302
262
                protected class SkipContactException extends Exception { }
303
263
 
304
264
                public void parseLine( String line )
305
265
                                throws ParseException, SkipContactException,
306
266
                                AbortImportException
307
267
                {
308
 
                        // ignore empty lines
309
 
                        if( line.trim() == "" ) return;
310
 
 
311
 
                        // split line into name and value parts (this may turn out to be
312
 
                        // unwanted if the line is a subsequent line in a multi-line
313
 
                        // value, but we have to do this now to check for and handle VCF
314
 
                        // versions first)
315
 
                        String[] props = line.split(  ":", 2 );
 
268
                        // get property halves
 
269
                        String[] props = line.split( ":" );
316
270
                        for( int i = 0; i < props.length; i++ )
317
271
                                props[ i ] = props[ i ].trim();
 
272
                        if( props.length < 2 ||
 
273
                                        props[ 0 ].length() < 1 || props[ 1 ].length() < 1 )
 
274
                                throw new ParseException( R.string.error_vcf_malformed );
318
275
 
319
 
                        // if we haven't yet got a version, we won't be paring anything!
320
276
                        if( _version == null )
321
277
                        {
322
 
                                // is this a version?
323
 
                                if( props.length == 2 && props[ 0 ].equals( "VERSION" ) )
 
278
                                if( props[ 0 ].equals( "VERSION" ) )
324
279
                                {
325
 
                                        // yes, check/store it
 
280
                                        // get version
326
281
                                        if( !props[ 1 ].equals( "2.1" ) &&
327
282
                                                        !props[ 1 ].equals( "3.0" ) )
328
283
                                                throw new ParseException( R.string.error_vcf_version );
336
291
                                }
337
292
                                else
338
293
                                {
339
 
                                        // no, so stash this line till we have a version
 
294
                                        // stash this line till we have a version
340
295
                                        if( _lines == null )
341
296
                                                _lines = new Vector< String >();
342
297
                                        _lines.add( line );
344
299
                        }
345
300
                        else
346
301
                        {
347
 
                                if( _parser_in_multiline )
348
 
                                {
349
 
                                        // if we're currently in a multi-line value, use the stored
350
 
                                        // property name and parameters
351
 
                                        props = new String[ 2 ];
352
 
                                        props[ 0 ] = _parser_current_name_and_params;
353
 
                                        props[ 1 ] = line.trim();
354
 
                                }
355
 
                                else
356
 
                                {
357
 
                                        // for normal lines, check the property name/value bits
358
 
                                        if( props.length < 2 || props[ 0 ].length() == 0 )
359
 
                                                throw new ParseException(
360
 
                                                        R.string.error_vcf_malformed );
361
 
 
362
 
                                        // ignore empty properties
363
 
                                        if( props[ 1 ].length() < 1 )
364
 
                                                return;
365
 
 
366
 
                                        // reset the saved multi-line state
367
 
                                        _parser_current_name_and_params = props[ 0 ];
368
 
                                        _parser_buffered_value_so_far = "";
369
 
                                }
370
 
 
371
302
                                // get parameter parts
372
303
                                String[] params = props[ 0 ].split( ";" );
373
304
                                for( int i = 0; i < params.length; i++ )
374
305
                                        params[ i ] = params[ i ].trim();
375
306
 
376
 
                                // parse charset and encoding parameters
377
 
                                String charset, encoding;
378
 
                                if( ( charset = checkParam( params, "CHARSET" ) ) != null &&
379
 
                                        !charset.equals( "UTF-8" ) && !charset.equals( "UTF-16" ) )
380
 
                                {
381
 
                                        throw new ParseException( R.string.error_vcf_charset );
382
 
                                }
383
 
                                if( ( encoding = checkParam( params, "ENCODING" ) ) != null &&
384
 
                                        !encoding.equals( "QUOTED-PRINTABLE" ) &&
385
 
                                        !encoding.equals( "8BIT" ) )
386
 
                                        //&& !encoding.equals( "BASE64" ) )
387
 
                                {
388
 
                                        throw new ParseException( R.string.error_vcf_encoding );
389
 
                                }
390
 
 
391
 
                                // do unencoding (or default to a fake unencoding result with
392
 
                                // the raw string)
393
 
                                UnencodeResult result;
394
 
                                if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
395
 
                                        result = unencodeQuotedPrintable( props[ 1 ], charset );
396
 
//                              else if( encoding != null && encoding.equals( "BASE64" ) )
397
 
//                                      result = unencodeBase64( props[ 1 ], charset );
398
 
                                else
399
 
                                        result = new UnencodeResult( false, props[ 1 ].getBytes(),
400
 
                                                props[ 1 ].getBytes().length );
401
 
 
402
 
                                // process charset
403
 
                                try {
404
 
                                        props[ 1 ] = new String( result.getBytes(), 0,
405
 
                                                result.getNumBytes(),
406
 
                                                charset == null? "UTF-8" : charset );
407
 
                                } catch( UnsupportedEncodingException e ) {
408
 
                                        throw new ParseException( R.string.error_vcf_charset );
409
 
                                }
410
 
 
411
 
                                // handle multi-line requests
412
 
                                _parser_in_multiline = result.isAnotherLineRequired();
413
 
                                if( _parser_in_multiline ) {
414
 
                                        _parser_buffered_value_so_far += props[ 1 ];
415
 
                                        return;
416
 
                                }
417
 
 
418
 
                                // add on buffered multi-line content
419
 
                                String value = _parser_buffered_value_so_far + props[ 1 ];
420
 
 
421
307
                                // parse some properties
422
308
                                if( params[ 0 ].equals( "N" ) )
423
 
                                        parseN( params, value );
 
309
                                        parseN( params, props[ 1 ] );
424
310
                                else if( params[ 0 ].equals( "FN" ) )
425
 
                                        parseFN( params, value );
 
311
                                        parseFN( params, props[ 1 ] );
426
312
                                else if( params[ 0 ].equals( "ORG" ) )
427
 
                                        parseORG( params, value );
 
313
                                        parseORG( params, props[ 1 ] );
428
314
                                else if( params[ 0 ].equals( "TEL" ) )
429
 
                                        parseTEL( params, value );
 
315
                                        parseTEL( params, props[ 1 ] );
430
316
                                else if( params[ 0 ].equals( "EMAIL" ) )
431
 
                                        parseEMAIL( params, value );
 
317
                                        parseEMAIL( params, props[ 1 ] );
432
318
                        }
433
319
                }
434
320
 
437
323
                                AbortImportException
438
324
                {
439
325
                        // already got a better name?
440
 
                        if( _name_level >= NAMELEVEL_N ) return;
 
326
                        if( _nameLevel >= NAMELEVEL_N ) return;
441
327
 
442
328
                        // get name parts
443
329
                        String[] nameparts = value.split( ";" );
452
338
                                value += ( value.length() == 0? "" : " " ) + nameparts[ 0 ];
453
339
 
454
340
                        // set name
455
 
                        setName( value );
456
 
                        _name_level = NAMELEVEL_N;
 
341
                        setName( undoCharsetAndEncoding( params, value ) );
 
342
                        _nameLevel = NAMELEVEL_N;
457
343
 
458
344
                        // check now to see if we need to import this contact (to avoid
459
345
                        // parsing the rest of the vCard unnecessarily)
465
351
                                throws ParseException, SkipContactException
466
352
                {
467
353
                        // already got a better name?
468
 
                        if( _name_level >= NAMELEVEL_FN ) return;
 
354
                        if( _nameLevel >= NAMELEVEL_FN ) return;
469
355
 
470
356
                        // set name
471
 
                        setName( value );
472
 
                        _name_level = NAMELEVEL_FN;
 
357
                        setName( undoCharsetAndEncoding( params, value ) );
 
358
                        _nameLevel = NAMELEVEL_FN;
473
359
                }
474
360
 
475
361
                private void parseORG( String[] params, String value )
476
362
                                throws ParseException, SkipContactException
477
363
                {
478
364
                        // already got a better name?
479
 
                        if( _name_level >= NAMELEVEL_ORG ) return;
 
365
                        if( _nameLevel >= NAMELEVEL_ORG ) return;
480
366
 
481
367
                        // get org parts
482
368
                        String[] orgparts = value.split( ";" );
490
376
                                value = orgparts[ 0 ];
491
377
 
492
378
                        // set name
493
 
                        setName( value );
494
 
                        _name_level = NAMELEVEL_ORG;
 
379
                        setName( undoCharsetAndEncoding( params, value ) );
 
380
                        _nameLevel = NAMELEVEL_ORG;
495
381
                }
496
382
 
497
383
                private void parseTEL( String[] params, String value )
522
408
                }
523
409
 
524
410
                public void parseEMAIL( String[] params, String value )
525
 
                                throws ParseException
526
411
                {
527
412
                        if( value.length() == 0 ) return;
528
413
 
546
431
                                throw new ParseException( R.string.error_vcf_malformed );
547
432
 
548
433
                        //  missing name properties?
549
 
                        if( _name_level == NAMELEVEL_NONE )
 
434
                        if( _nameLevel == NAMELEVEL_NONE )
550
435
                                throw new ParseException( R.string.error_vcf_noname );
551
436
 
552
437
                        // check if we should import this one? If we've already got an 'N'-
553
438
                        // type name, this will already have been done by parseN() so we
554
439
                        // mustn't do this here (or it could prompt twice!)
555
 
                        if( _name_level < NAMELEVEL_N && !isImportRequired( getName() ) )
 
440
                        if( _nameLevel < NAMELEVEL_N && !isImportRequired( getName() ) )
556
441
                                throw new SkipContactException();
557
442
                }
558
443
 
 
444
                private String undoCharsetAndEncoding( String[] params, String value )
 
445
                                throws ParseException
 
446
                {
 
447
                        // check encoding/charset
 
448
                        String charset, encoding;
 
449
                        if( ( charset = checkParam( params, "CHARSET" ) ) != null &&
 
450
                                        !charset.equals( "UTF-8" ) && !charset.equals( "UTF-16" ) )
 
451
                                throw new ParseException( R.string.error_vcf_charset );
 
452
                        if( ( encoding = checkParam( params, "ENCODING" ) ) != null &&
 
453
                                        !encoding.equals( "QUOTED-PRINTABLE" ) )
 
454
                                throw new ParseException( R.string.error_vcf_encoding );
 
455
 
 
456
                        // do decoding?
 
457
                        if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
 
458
                                return unencodeQuotedPrintable( value, charset );
 
459
 
 
460
                        // nothing to do!
 
461
                        return value;
 
462
                }
 
463
 
559
464
                private String checkParam( String[] params, String name )
560
465
                {
561
466
                        Pattern p = Pattern.compile( "^" + name + "[ \\t]*=[ \\t]*(.*)$" );
591
496
                        return types;
592
497
                }
593
498
 
594
 
                private UnencodeResult unencodeQuotedPrintable( String str, String charset )
 
499
                private String unencodeQuotedPrintable( String str, String charset )
595
500
                {
596
 
                        boolean another = false;
597
 
 
598
501
                        // default encoding scheme
599
502
                        if( charset == null ) charset = "UTF-8";
600
503
 
601
504
                        // unencode quoted-pritable encoding, as per RFC1521 section 5.1
602
505
                        byte[] bytes = new byte[ str.length() ];
603
506
                        int j = 0;
604
 
                        for( int i = 0; i < str.length(); i++ )
605
 
                        {
606
 
                                // get next char and process...
 
507
                        for( int i = 0; i < str.length(); i++, j++ ) {
607
508
                                char ch = str.charAt( i );
608
 
                                if( ch == '=' && i < str.length() - 2 )
609
 
                                {
610
 
                                        // we found a =XX format byte, add it
 
509
                                if( ch == '=' && i < str.length() - 2 ) {
611
510
                                        bytes[ j ] = (byte)(
612
511
                                                        Character.digit( str.charAt( i + 1 ), 16 ) * 16 +
613
512
                                                        Character.digit( str.charAt( i + 2 ), 16 ) );
614
513
                                        i += 2;
615
514
                                }
616
 
                                else if( ch == '=' && i == str.length() - 1 )
617
 
                                {
618
 
                                        // we found a '=' at the end of a line signifying a multi-
619
 
                                        // line string, so we don't add it.
620
 
                                        another = true;
621
 
                                        continue;
622
 
                                }
623
515
                                else
624
 
                                        // just a normal char...
625
516
                                        bytes[ j ] = (byte)ch;
626
 
                                j++;
627
517
                        }
628
 
 
629
 
                        return new UnencodeResult( another, bytes, j );
 
518
                        try {
 
519
                                return new String( bytes, 0, j, charset );
 
520
                        } catch( UnsupportedEncodingException e ) { }
 
521
                        return null;
630
522
                }
631
523
        }
632
524
}